Открыть меню
Переключить меню настроек
Открыть персональное меню
Вы не представились системе
Ваш IP-адрес будет виден всем, если вы внесёте какие-либо изменения.

MediaWiki:ScmcChemistry.js: различия между версиями

Страница интерфейса MediaWiki
Нет описания правки
Нет описания правки
 
(не показаны 4 промежуточные версии этой же участницы)
Строка 2: Строка 2:
  * Интерактивный каталог химии 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 METABOLISM_LABELS = {
         Alcohol: 'Алкоголь',
        Drink: 'Напиток',
        Food: 'Пища',
        Medicine: 'Лекарство',
        Narcotic: 'Наркотик',
        Poison: 'Яд'
     };
     var DAMAGE_LABELS = {
     var DAMAGE_LABELS = {
         Airloss: 'урона от кислородного голодания',
         Airloss: 'урона от кислородного голодания',
Строка 43: Строка 49:
         Toxin: 'токсического урона'
         Toxin: 'токсического урона'
     };
     };
    /*
    * Смысловые фрагменты в описаниях эффектов. Цвет не заменяет текст:
    * он лишь помогает быстро отличить лечение, вред и конкретный тип урона.
    */
     var EFFECT_TOKEN_RE = /(урона от кислородного голодания|урона от кровопотери|полностью лечит|дополнительно наносит|восстанавливает жизнеспособность|создаёт иммунитет|восстанавливает|возвращает|омолаживает|сокращает|удаляет|лечит|наносит|вызывает|заражает|поджигает|уничтожает|расходует|замедляет|снижает|повышает|механического(?: урона)?|тупого(?: урона)?|колотого(?: урона)?|режущего(?: урона)?|термического(?: урона)?|ожогового(?: урона)?|холодового(?: урона)?|токсического(?: урона)?|кислотного(?: урона)?|генетического(?: урона)?|клеточного(?: урона)?|радиационного(?: урона)?)/gi;
     var EFFECT_TOKEN_RE = /(урона от кислородного голодания|урона от кровопотери|полностью лечит|дополнительно наносит|восстанавливает жизнеспособность|создаёт иммунитет|восстанавливает|возвращает|омолаживает|сокращает|удаляет|лечит|наносит|вызывает|заражает|поджигает|уничтожает|расходует|замедляет|снижает|повышает|механического(?: урона)?|тупого(?: урона)?|колотого(?: урона)?|режущего(?: урона)?|термического(?: урона)?|ожогового(?: урона)?|холодового(?: урона)?|токсического(?: урона)?|кислотного(?: урона)?|генетического(?: урона)?|клеточного(?: урона)?|радиационного(?: урона)?)/gi;
     var EFFECT_ACTION_CLASSES = {
     var EFFECT_ACTION_CLASSES = {
         'полностью лечит': 'beneficial',
         'полностью лечит': 'beneficial', 'лечит': 'beneficial',
        'лечит': 'beneficial',
         'восстанавливает': 'beneficial', 'восстанавливает жизнеспособность': 'beneficial',
         'восстанавливает': 'beneficial',
         'возвращает': 'beneficial', 'омолаживает': 'beneficial', 'сокращает': 'beneficial',
        'восстанавливает жизнеспособность': 'beneficial',
         'удаляет': 'beneficial', 'создаёт иммунитет': 'beneficial',
         'возвращает': 'beneficial',
         'наносит': 'harmful', 'дополнительно наносит': 'harmful', 'вызывает': 'harmful',
        'омолаживает': 'beneficial',
         'заражает': 'harmful', 'поджигает': 'harmful', 'уничтожает': 'harmful',
        'сокращает': 'beneficial',
         'расходует': 'warning', 'замедляет': 'warning',
         'удаляет': 'beneficial',
         'снижает': 'utility', 'повышает': 'utility'
        'создаёт иммунитет': 'beneficial',
         'наносит': 'harmful',
        'дополнительно наносит': 'harmful',
        'вызывает': 'harmful',
         'заражает': 'harmful',
        'поджигает': 'harmful',
        'уничтожает': 'harmful',
         'расходует': 'warning',
        'замедляет': 'warning',
         'снижает': 'utility',
        'повышает': 'utility'
     };
     };
     var DAMAGE_TOKEN_CLASSES = [
     var DAMAGE_TOKEN_CLASSES = [
         { pattern: /кислородного голодания|кровопотери/, className: 'oxygen' },
         { pattern: /кислородного голодания|кровопотери/, className: 'oxygen' },
Строка 83: Строка 71:
         { pattern: /радиационного/, className: 'radiation' }
         { pattern: /радиационного/, className: 'radiation' }
     ];
     ];
     var STATUS_LABELS = {
     var STATUS_LABELS = {
         Jitter: 'дрожь',
         Jitter: 'дрожь', RMCUnconscious: 'потерю сознания',
        RMCUnconscious: 'потерю сознания',
         StatusEffectSeeingRainbow: 'галлюцинации', Unconscious: 'потерю сознания'
         StatusEffectSeeingRainbow: 'галлюцинации',
        Unconscious: 'потерю сознания'
     };
     };
     var EMOTE_LABELS = {
     var EMOTE_LABELS = {
         Gasp: 'судорожный вдох',
         Gasp: 'судорожный вдох', Laugh: 'смех', Scream: 'крик', Yawn: 'зевоту'
        Laugh: 'смех',
        Scream: 'крик',
        Yawn: 'зевоту'
     };
     };
     var PLANT_ATTRIBUTE_LABELS = {
     var PLANT_ATTRIBUTE_LABELS = {
         PlantAdjustGrowth: 'возраст растения',
         PlantAdjustGrowth: 'возраст растения', PlantAdjustHealth: 'здоровье растения',
        PlantAdjustHealth: 'здоровье растения',
         PlantAdjustMutationLevel: 'уровень мутации', PlantAdjustMutationMod: 'модификатор мутации',
         PlantAdjustMutationLevel: 'уровень мутации',
         PlantAdjustNutrition: 'уровень питания', PlantAdjustPests: 'уровень вредителей',
        PlantAdjustMutationMod: 'модификатор мутации',
         PlantAdjustPotency: 'потенцию растения', PlantAdjustToxins: 'уровень токсинов',
         PlantAdjustNutrition: 'уровень питания',
         PlantAdjustWater: 'уровень воды', PlantAdjustWeeds: 'уровень сорняков',
        PlantAdjustPests: 'уровень вредителей',
         PlantAdjustPotency: 'потенцию растения',
        PlantAdjustToxins: 'уровень токсинов',
         PlantAdjustWater: 'уровень воды',
        PlantAdjustWeeds: 'уровень сорняков',
         PlantAffectGrowth: 'возраст растения'
         PlantAffectGrowth: 'возраст растения'
     };
     };
 
     var REAGENT_LABELS = {
     var METABOLISM_LABELS = {
         UncookedAnimalProteins: 'сырые животные белки'
         Alcohol: 'Алкоголь',
        Drink: 'Напиток',
        Food: 'Пища',
        Medicine: 'Лекарство',
        Narcotic: 'Наркотик',
        Poison: 'Яд'
     };
     };
     var state = {
     var state = {
         data: null,
         data: null,
Строка 128: Строка 96:
     };
     };


     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;
     }
     }
Строка 160: Строка 122:
     function formatAmount(value) {
     function formatAmount(value) {
         return formatNumber(value) + ' ед.';
         return formatNumber(value) + ' ед.';
    }
    function normalize(value) {
        return String(value || '').toLocaleLowerCase('ru-RU').replace(/ё/g, 'е').trim();
     }
     }


     function capitalizeFirst(value) {
     function capitalizeFirst(value) {
        var text = String(value || '').trim();
        return text ? text.charAt(0).toLocaleUpperCase('ru-RU') + text.slice(1) : text;
    }
    function lowerFirst(value) {
         var text = String(value || '');
         var text = String(value || '');
        return text ? text.charAt(0).toLocaleLowerCase('ru-RU') + text.slice(1) : text;
    }
    function finishSentence(value) {
        var text = capitalizeFirst(String(value || '').trim());
        return !text || /[.!?]$/.test(text) ? text : text + '.';
    }
    function tagName(tag) {
        return String(tag || '').replace(/^!type:/, '');
    }
    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 reagentName(id) {
        var reagent = getReagent(id);
        return reagent && reagent.name ? reagent.name : REAGENT_LABELS[id] || id;
    }
    function getName(id) {
        var reagent = getReagent(id);
        return capitalizeFirst(reagent && reagent.name ? reagent.name : REAGENT_LABELS[id] || id);
    }
    function getColor(id) {
        var reagent = getReagent(id);
        return safeColor(reagent && reagent.properties && reagent.properties.color);
    }


         if (!text) {
    function indexReactions() {
             return text;
        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',
            maxage: 0,
            smaxage: 0
        }), 'text/css');
    }
 
    function loadCatalog() {
        window.ScmcChemCatalog = null;
        return mw.loader.getScript(mw.util.getUrl(CATALOG_PAGE, {
            action: 'raw',
            ctype: 'text/javascript',
            maxage: 0,
            smaxage: 0
        })).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);
         }
         }
    }


         return text.charAt(0).toLocaleUpperCase('ru-RU') + text.slice(1);
    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 finishSentence(value) {
     function renderTabs(root) {
         var text = capitalizeFirst(String(value || '').trim());
         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]; });
    }


         if (!text || /[.!?]$/.test(text)) {
    function entryMatches(entry) {
             return text;
         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', '', capitalizeFirst(group.title)),
                el('span', 'scmc-chem__section-link-count', group.entries.length)
            );
            add(nav, link);
        });
    }


         return text + '.';
    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', capitalizeFirst(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 safeColor(value) {
     function renderReaction(reaction) {
         if (typeof value !== 'string') {
         var box = el('div', 'scmc-chem__reaction');
             return '#7f8c8d';
        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;
    }


         if (/^#[0-9a-f]{3,8}$/i.test(value) || /^[a-z]+$/i.test(value)) {
    function effectType(effect) {
            return value;
         return String(effect && effect.yamlTag || '').replace(/^!type:/, '') || 'Эффект';
        }
    }
 
    function action(text, kind) {
        return el('span', 'scmc-chem__effect-action scmc-chem__effect-action--' + kind, text);
    }


         return '#7f8c8d';
    function damageType(type) {
        var className = DAMAGE_CLASSES[type] || 'physical';
         return el('span', 'scmc-chem__damage-type scmc-chem__damage-type--' + className, DAMAGE_LABELS[type] || type);
     }
     }


     function tagName(tag) {
     function line() {
         return String(tag || '').replace(/^!type:/, '');
         var node = el('div', 'scmc-chem__effect-details');
        Array.prototype.slice.call(arguments).forEach(function (part) {
            node.appendChild(part && part.nodeType ? part : document.createTextNode(String(part)));
        });
        return node;
    }
 
    function probabilityText(value) {
        return value === undefined ? '' : ' · вероятность ' + formatNumber(value * 100) + '%';
     }
     }


     function reagentById(id) {
     function damageEntries(value) {
         return state.data.reagents[id] || state.data.dependencies[id] || null;
         var damage = value && value.damage;
        var result = [];
        if (Array.isArray(damage)) {
            damage.forEach(function (item) {
                Object.keys(item || {}).forEach(function (type) {
                    result.push({ type: type, amount: item[type] });
                });
            });
        } else if (damage && typeof damage === 'object') {
            ['groups', 'types'].forEach(function (section) {
                Object.keys(damage[section] || {}).forEach(function (type) {
                    result.push({ type: type, amount: damage[section][type] });
                });
            });
            if (!damage.groups && !damage.types) {
                Object.keys(damage).forEach(function (type) {
                    if (typeof damage[type] === 'number') {
                        result.push({ type: type, amount: damage[type] });
                    }
                });
            }
        }
        return result;
     }
     }


     function reagentName(id) {
     function effectKind(type, value) {
         var reagent = reagentById(id);
         var beneficial = /^(Anticorrosive|Antihallucinogenic|Antitoxic|Biocidic|ChemHealEyeDamage|CureZombieInfection|Electrogenetic|Focusing|Hemogenic|Neogenetic|Nutritious|Oxygenating|SatiateHunger|SatiateThirst|Thermostabilizing)$/;
         return reagent && reagent.name ? reagent.name : id;
         var harmful = /^(Carcinogenic|CauseZombieInfection|ChemVomit|Corrosive|Fueling|Hypoxemic|Jitter|Ketogenic|Oxidizing|Toxic|Viscous)$/;
        if (type === 'HealthChange' || type === 'EqualHealthChange') {
            var damage = damageEntries(value);
            return damage.length && damage.every(function (item) { return item.amount <= 0; }) ? 'beneficial' :
                damage.length && damage.every(function (item) { return item.amount >= 0; }) ? 'harmful' : 'warning';
        }
        if (beneficial.test(type)) {
            return 'beneficial';
        }
        if (harmful.test(type)) {
            return 'harmful';
        }
        return 'warning';
     }
     }


     function normalizeSearch(value) {
     function conditionText(condition, reagent, hideDoseCondition) {
         return String(value || '').trim().toLocaleLowerCase('ru-RU');
        var type = effectType(condition);
        var value = condition.value || {};
        if (type === 'ReagentThreshold') {
            if (hideDoseCondition) {
                return '';
            }
            return 'при содержании ' + (value.reagent ? getName(value.reagent) + ' ' : '') +
                (value.min !== undefined ? 'от ' + formatAmount(value.min) : '') +
                (value.max !== undefined ? ' до ' + formatAmount(value.max) : '');
        }
         if (type === 'Temperature') {
            return 'при температуре ' +
                (value.min !== undefined ? 'от ' + formatNumber(value.min) + ' K' : '') +
                (value.max !== undefined ? 'до ' + formatNumber(value.max) + ' K' : '');
        }
        if (type === 'MobStateCondition') {
            var stateName = value.mobstate || value.state || 'особое';
            return 'если цель находится в состоянии: ' + (MOB_STATE_LABELS[stateName] || stateName);
        }
        if (type === 'OrganType') {
            var organ = value.type || value.organType || 'подходящего типа';
            return 'для организма ' + (ORGAN_LABELS[organ] || organ);
        }
        return 'при выполнении дополнительного условия';
     }
     }


     function matchesQuery(entry, reagent) {
     function renderEffect(effect, reagent, tier) {
         if (!state.query) {
        var type = effectType(effect);
             return true;
        var value = effect.value && typeof effect.value === 'object' ? effect.value : {};
        var kind = effectKind(type, value);
        var card = el('div', 'scmc-chem__effect scmc-chem__effect--' + kind);
        add(card, el('div', 'scmc-chem__effect-name', EFFECT_LABELS[type] || ('Эффект «' + type + '»')));
 
        var damage = damageEntries(value);
         if (damage.length) {
            damage.forEach(function (item) {
                var healing = item.amount < 0;
                var oxygenLike = /^(Airloss|Asphyxiation|Bloodloss)$/.test(item.type);
                add(card, oxygenLike ? line(
                    action(healing ? 'Лечит' : 'Наносит', healing ? 'beneficial' : 'harmful'),
                    ' ' + formatNumber(Math.abs(item.amount)) + ' ед. урона от ', damageType(item.type)
                ) : line(
                    action(healing ? 'Лечит' : 'Наносит', healing ? 'beneficial' : 'harmful'),
                    ' ' + formatNumber(Math.abs(item.amount)) + ' ед. ', damageType(item.type), ' урона'
                ));
            });
        } else if (/^(Antitoxic|Toxic)$/.test(type)) {
            add(card, line(action(type === 'Antitoxic' ? 'Лечит' : 'Наносит', type === 'Antitoxic' ? 'beneficial' : 'harmful'),
                ' ' + formatNumber(value.potency || 1) + ' ед. ', damageType('Toxin'), ' урона'));
        } else if (/^(Biocidic|Carcinogenic)$/.test(type)) {
            add(card, line(action(type === 'Biocidic' ? 'Лечит' : 'Наносит', type === 'Biocidic' ? 'beneficial' : 'harmful'),
                ' ' + formatNumber(value.potency || 1) + ' ед. ', damageType('Cellular'), ' урона'));
        } else if (/^(Neogenetic|Electrogenetic)$/.test(type)) {
            add(card, line(action('Лечит', 'beneficial'), ' ' + formatNumber(value.potency || 1) + ' ед. ', damageType(type === 'Neogenetic' ? 'Brute' : 'Genetic'), ' урона'));
        } else if (/^(Anticorrosive|Corrosive)$/.test(type)) {
            add(card, line(action(type === 'Anticorrosive' ? 'Лечит' : 'Наносит', type === 'Anticorrosive' ? 'beneficial' : 'harmful'),
                ' ' + formatNumber(value.potency || 1) + ' ед. ', damageType('Caustic'), ' урона'));
        } else if (/^(Oxygenating|Hypoxemic)$/.test(type)) {
            add(card, line(action(type === 'Oxygenating' ? 'Лечит' : 'Наносит', type === 'Oxygenating' ? 'beneficial' : 'harmful'),
                ' ' + formatNumber(value.potency || 1) + ' ед. урона от ', damageType('Airloss')));
        } else if (type === 'Oxidizing') {
            add(card, line(action('Наносит', 'harmful'), ' ' + formatNumber(value.potency || 1) + ' ед. ', damageType('Burn'), ' урона'));
        } else if (type === 'Hemogenic') {
            add(card, line(action('Восстанавливает', 'beneficial'), ' кровь · сила ' + formatNumber(value.potency || 1)));
        } else if (type === 'AdjustReagent') {
            var amount = Number(value.amount || 0);
            add(card, line(action(amount < 0 ? 'Удаляет' : 'Добавляет', amount < 0 ? 'beneficial' : 'utility'),
                ' ' + formatAmount(Math.abs(amount)) + ' реагента «' + getName(value.reagent) + '»'));
        } else if (type === 'GenericStatusEffect' || type === 'ModifyStatusEffect') {
            var status = value.component || value.key || value.effectProto || 'статусный эффект';
            add(card, line(action(value.type === 'Remove' ? 'Снимает' : 'Вызывает', value.type === 'Remove' ? 'beneficial' : 'harmful'),
                ' ' + (STATUS_LABELS[status] || status) + (value.time !== undefined ? ' на ' + formatNumber(value.time) + ' с' : '')));
        } else if (type === 'Emote') {
            add(card, line(action('Вызывает', 'warning'), ' ' + (EMOTE_LABELS[value.emote] || value.emote || 'непроизвольную реакцию') + probabilityText(value.probability)));
        } else if (type === 'Jitter') {
            add(card, line(action('Вызывает', 'harmful'), ' дрожь' + probabilityText(value.probability)));
        } else if (type === 'ChemVomit') {
            add(card, line(action('Вызывает', 'harmful'), ' рвоту' + probabilityText(value.probability)));
        } else if (type === 'SatiateThirst' || type === 'SatiateHunger') {
            add(card, line(action(type === 'SatiateThirst' ? 'Утоляет жажду' : 'Утоляет голод', 'beneficial'),
                value.factor !== undefined ? ' · коэффициент ' + formatNumber(value.factor) : ''));
        } else if (type === 'Drunk') {
            add(card, line(action('Опьяняет', 'warning'), value.boozePower !== undefined ? ' · сила ' + formatNumber(value.boozePower) : ''));
        } else if (type === 'MovespeedModifier') {
            add(card, line(action('Изменяет скорость', 'utility'),
                ' · ходьба ×' + formatNumber(value.walkSpeedModifier || 1) + ' · бег ×' + formatNumber(value.sprintSpeedModifier || 1)));
        } else if (type === 'CureZombieInfection' || type === 'CauseZombieInfection') {
            add(card, line(action(type === 'CureZombieInfection' ? 'Лечит' : 'Заражает', type === 'CureZombieInfection' ? 'beneficial' : 'harmful'),
                (type === 'CureZombieInfection' ? ' зомби-инфекцию' : ' зомби-инфекцией') + (value.innoculate ? ' и создаёт иммунитет' : '')));
        } else if (type === 'PopupMessage') {
             add(card, line(action('Показывает', 'utility'), ' игровое сообщение' + probabilityText(value.probability)));
        } else {
            var potency = value.potency !== undefined ? ' · сила ' + formatNumber(value.potency) : '';
            var multiplier = value.multiplier !== undefined ? ' · множитель ' + formatNumber(value.multiplier) : '';
            add(card, line(action(EFFECT_LABELS[type] || 'Воздействует', kind === 'beneficial' ? 'beneficial' : kind === 'harmful' ? 'harmful' : 'utility'), potency + multiplier + probabilityText(value.probability)));
         }
         }


         return normalizeSearch([
         (value.conditions || []).forEach(function (condition) {
            entry.id,
             var text = conditionText(condition, reagent, tier !== 'normal' && effectType(condition) === 'ReagentThreshold');
             entry.name,
            if (text) {
            reagent && reagent.name,
                add(card, el('div', 'scmc-chem__effect-condition', 'Условие: ' + text));
            reagent && reagent.description,
            }
            entry.sectionPath && entry.sectionPath.join(' ')
        });
        ].filter(Boolean).join(' ')).indexOf(state.query) !== -1;
        return card;
     }
     }


     function buildProductReactionIndex() {
     function effectDose(effect, reagent) {
         state.productReactions = Object.create(null);
         var conditions = effect.value && effect.value.conditions || [];
        var mins = conditions.filter(function (condition) {
            var value = condition.value || {};
            return effectType(condition) === 'ReagentThreshold' &&
                (!value.reagent || value.reagent === reagent.id) && value.min !== undefined;
        }).map(function (condition) { return Number(condition.value.min); });
        return mins.length ? Math.max.apply(Math, mins) : null;
    }


         Object.keys(state.data.reactions).forEach(function (reactionId) {
    function effectTier(effect, reagent) {
             var reaction = state.data.reactions[reactionId];
         var dose = effectDose(effect, reagent);
        var properties = reagent.properties || {};
        if (dose !== null && properties.criticalOverdose !== undefined && dose >= properties.criticalOverdose) {
             return 'critical';
        }
        if (dose !== null && properties.overdose !== undefined && dose >= properties.overdose) {
            return 'overdose';
        }
        return 'normal';
    }


            (reaction.products || []).forEach(function (product) {
    function tierTitle(tier, reagent) {
                if (!state.productReactions[product.id]) {
        var properties = reagent.properties || {};
                    state.productReactions[product.id] = [];
        if (tier === 'critical') {
                }
            return 'Критическая передозировка' + (properties.criticalOverdose !== undefined ? ' · от ' + formatAmount(properties.criticalOverdose) : '');
        }
        if (tier === 'overdose') {
            return 'Передозировка' + (properties.overdose !== undefined ? ' · от ' + formatAmount(properties.overdose) : '');
        }
        return 'Обычная доза' + (properties.overdose !== undefined ? ' · до ' + formatAmount(properties.overdose) : '');
    }


                state.productReactions[product.id].push(reaction);
    function renderEffects(effects, reagent) {
        var list = el('div', 'scmc-chem__effect-list');
        if (!effects || !effects.length) {
            add(list, el('p', 'scmc-chem__empty', 'Эффекты не указаны.'));
            return list;
        }
        var tiers = { normal: [], overdose: [], critical: [] };
        effects.forEach(function (effect) {
            tiers[effectTier(effect, reagent)].push(effect);
        });
        ['normal', 'overdose', 'critical'].forEach(function (tier) {
            if (!tiers[tier].length) {
                return;
            }
            var block = el('div', 'scmc-chem__effect-tier scmc-chem__effect-tier--' + tier);
            add(block, el('div', 'scmc-chem__effect-tier-title', tierTitle(tier, reagent)));
            tiers[tier].forEach(function (effect) {
                add(block, renderEffect(effect, reagent, tier));
             });
             });
            add(list, block);
         });
         });
        return list;
     }
     }


     function entriesForTab(tabId) {
     function plantEffectKind(type, amount) {
         return state.data.catalogSections[tabId] || [];
         if (/Mutation/.test(type)) {
            return 'warning';
        }
        var harmfulWhenPositive = /Toxins|Weeds|Pests/.test(type);
        return (harmfulWhenPositive ? amount <= 0 : amount >= 0) ? 'beneficial' : 'harmful';
     }
     }


     function effectValue(effect) {
     function renderPlantEffects(effects) {
        var list = el('div', 'scmc-chem__effect-list');
        (effects || []).forEach(function (effect) {
            var type = effectType(effect);
            var value = effect.value || {};
            if (type === 'PlantCryoxadone') {
                var special = el('div', 'scmc-chem__effect scmc-chem__effect--beneficial');
                add(special, el('div', 'scmc-chem__effect-name', 'Криогенное восстановление'));
                add(special, line(action('Омолаживает', 'beneficial'), ' растение при низкой температуре'));
                add(list, special);
                return;
            }
            var amount = Number(value.amount || 0);
            var kind = plantEffectKind(type, amount);
            var card = el('div', 'scmc-chem__effect scmc-chem__effect--' + kind);
            add(card, el('div', 'scmc-chem__effect-name', capitalizeFirst(PLANT_LABELS[type] || ('Эффект «' + type + '»'))));
            add(card, line(
                action(amount >= 0 ? 'Повышает' : 'Снижает', kind === 'beneficial' ? 'beneficial' : kind === 'harmful' ? 'harmful' : 'warning'),
                ' ' + (PLANT_LABELS[type] || 'показатель') + ' на ' + formatNumber(Math.abs(amount)) +
                    probabilityText(value.probability)
            ));
            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, reagent));
            add(wrapper, block);
        });
        return wrapper;
    }
 
    function renderPlantMetabolism(reagent) {
        var effects = reagent.properties && reagent.properties.plantMetabolism;
        if (!Array.isArray(effects) || !effects.length) {
            return null;
        }
        var wrapper = el('div', 'scmc-chem__card-section');
        var block = el('div', 'scmc-chem__metabolism');
        add(wrapper, el('h4', 'scmc-chem__card-subtitle', 'Воздействие на растения'));
        add(block, el('div', 'scmc-chem__metabolism-title', 'Метаболизм растений'), renderPlantEffects(effects));
        add(wrapper, block);
        return wrapper;
    }
 
    /* Исходная реализация описаний из js.txt. */
    function originalEffectValue(effect) {
         var value = effect && effect.value;
         var value = effect && effect.value;
         return value && typeof value === 'object' ? value : {};
         return value && typeof value === 'object' ? value : {};
     }
     }


     function lowerFirst(value) {
     function originalWithChance(text, probability) {
        var text = String(value || '');
        return text ? text.charAt(0).toLocaleLowerCase('ru-RU') + text.slice(1) : text;
    }
 
    function withChance(text, probability) {
         if (typeof probability !== 'number' || probability >= 1) {
         if (typeof probability !== 'number' || probability >= 1) {
             return finishSentence(text);
             return finishSentence(text);
         }
         }
         return finishSentence('С шансом ' + formatNumber(probability * 100) + '% ' + lowerFirst(text));
         return finishSentence('С шансом ' + formatNumber(probability * 100) + '% ' + lowerFirst(text));
     }
     }


     function conditionText(condition) {
     function originalConditionText(condition) {
         var type = tagName(condition && condition.yamlTag);
         var type = tagName(condition && condition.yamlTag);
         var value = effectValue(condition);
         var value = originalEffectValue(condition);
 
         if (type === 'ReagentThreshold') {
         if (type === 'ReagentThreshold') {
             if (value.min !== undefined && value.max !== undefined) {
             if (value.min !== undefined && value.max !== undefined) {
Строка 278: Строка 680:
             }
             }
         }
         }
         if (type === 'Temperature') {
         if (type === 'Temperature') {
             if (value.min !== undefined && value.max !== undefined) {
             if (value.min !== undefined && value.max !== undefined) {
Строка 290: Строка 691:
             }
             }
         }
         }
         if (type === 'MobStateCondition') {
         if (type === 'MobStateCondition') {
             return value.mobstate === 'Critical' ? 'в критическом состоянии' : 'в состоянии «' + (value.mobstate || 'неизвестно') + '»';
             return value.mobstate === 'Critical' ? 'в критическом состоянии' : 'в состоянии «' + (value.mobstate || 'неизвестно') + '»';
         }
         }
         if (type === 'OrganType') {
         if (type === 'OrganType') {
             return (value.shouldHave === false ? 'кроме организмов типа ' : 'для организмов типа ') + (value.type || 'неизвестно');
             return (value.shouldHave === false ? 'кроме организмов типа ' : 'для организмов типа ') + (value.type || 'неизвестно');
         }
         }
         return type ? 'при выполнении условия «' + type + '»' : '';
         return type ? 'при выполнении условия «' + type + '»' : '';
     }
     }


     function thresholdMinimum(effect) {
     function originalThresholdMinimum(effect) {
         var conditions = effectValue(effect).conditions;
         var conditions = originalEffectValue(effect).conditions;
         var minimum = null;
         var minimum = null;
         if (!Array.isArray(conditions)) {
         if (!Array.isArray(conditions)) {
             return minimum;
             return minimum;
         }
         }
         conditions.forEach(function (condition) {
         conditions.forEach(function (condition) {
             if (tagName(condition && condition.yamlTag) !== 'ReagentThreshold') {
             if (tagName(condition && condition.yamlTag) !== 'ReagentThreshold') {
                 return;
                 return;
             }
             }
 
             var value = originalEffectValue(condition);
             var value = effectValue(condition);
             if (typeof value.min === 'number' && (minimum === null || value.min > minimum)) {
             if (typeof value.min === 'number' && (minimum === null || value.min > minimum)) {
                 minimum = value.min;
                 minimum = value.min;
             }
             }
         });
         });
         return minimum;
         return minimum;
     }
     }


     function effectTier(effect, reagent) {
     function originalEffectTier(effect, reagent) {
         var minimum = thresholdMinimum(effect);
         var minimum = originalThresholdMinimum(effect);
         var properties = reagent.properties || {};
         var properties = reagent.properties || {};
         if (minimum === null) {
         if (minimum === null) {
             return 'normal';
             return 'normal';
Строка 337: Строка 730:
             return 'overdose';
             return 'overdose';
         }
         }
         return 'normal';
         return 'normal';
     }
     }


     function remainingConditions(effect, reagent) {
     function originalRemainingConditions(effect, reagent) {
         var conditions = effectValue(effect).conditions;
         var conditions = originalEffectValue(effect).conditions;
 
         if (!Array.isArray(conditions)) {
         if (!Array.isArray(conditions)) {
             return [];
             return [];
         }
         }
         return conditions.map(function (condition) {
         return conditions.map(function (condition) {
             if (tagName(condition && condition.yamlTag) === 'ReagentThreshold' && effectTier(effect, reagent) !== 'normal') {
             if (tagName(condition && condition.yamlTag) === 'ReagentThreshold' && originalEffectTier(effect, reagent) !== 'normal') {
                 return '';
                 return '';
             }
             }
             return conditionText(condition);
             return originalConditionText(condition);
         }).filter(Boolean);
         }).filter(Boolean);
     }
     }


     function damageChanges(damage, equalGroups) {
     function originalDamageChanges(damage, equalGroups) {
         var entries = [];
         var entries = [];
         if (equalGroups && Array.isArray(damage)) {
         if (equalGroups && Array.isArray(damage)) {
             damage.forEach(function (entry) {
             damage.forEach(function (entry) {
Строка 369: Строка 758:
             ['groups', 'types'].forEach(function (key) {
             ['groups', 'types'].forEach(function (key) {
                 var values = damage[key];
                 var values = damage[key];
                 if (!values || typeof values !== 'object') {
                 if (!values || typeof values !== 'object') {
                     return;
                     return;
                 }
                 }
                 Object.keys(values).forEach(function (damageId) {
                 Object.keys(values).forEach(function (damageId) {
                     entries.push({ id: damageId, amount: values[damageId] });
                     entries.push({ id: damageId, amount: values[damageId] });
Строка 379: Строка 766:
             });
             });
         }
         }
         return entries.map(function (entry) {
         return entries.map(function (entry) {
             var action = entry.amount < 0 ? 'Лечит' : 'Наносит';
             var actionText = entry.amount < 0 ? 'Лечит' : 'Наносит';
             return action + ' ' + formatNumber(Math.abs(entry.amount)) + ' ед. ' + (DAMAGE_LABELS[entry.id] || ('урона типа «' + entry.id + '»')) + ' в секунду';
             return actionText + ' ' + formatNumber(Math.abs(entry.amount)) + ' ед. ' +
                (DAMAGE_LABELS[entry.id] || ('урона типа «' + entry.id + '»')) + ' в секунду';
         });
         });
     }
     }


     function doseDescription(tier, text) {
     function originalDoseDescription(tier, text) {
         return { tier: tier, text: finishSentence(text) };
         return { tier: tier, text: finishSentence(text) };
     }
     }


     function addDoseEffects(result, reagent, overdoseText, criticalText) {
     function originalAddDoseEffects(result, reagent, overdoseText, criticalText) {
         var properties = reagent.properties || {};
         var properties = reagent.properties || {};
         if (properties.overdose !== undefined && overdoseText) {
         if (properties.overdose !== undefined && overdoseText) {
             result.push(doseDescription('overdose', overdoseText));
             result.push(originalDoseDescription('overdose', overdoseText));
         }
         }
         if (properties.criticalOverdose !== undefined && criticalText) {
         if (properties.criticalOverdose !== undefined && criticalText) {
             result.push(doseDescription('critical', criticalText));
             result.push(originalDoseDescription('critical', criticalText));
         }
         }
     }
     }


     function rmcEffectDescriptions(type, value, reagent) {
     function originalRmcEffectDescriptions(type, value, reagent) {
         var result = [];
         var result = [];
         var potency = Number(value.potency || 0);
         var potency = Number(value.potency || 0);
         var actual = potency * 0.5;
         var actual = potency || 0.5;
         var perSecond = potency * 0.25;
         var perSecond = potency * 0.25;
         var healing;
         var healing;


         if (type === 'Neogenetic') {
         if (type === 'Neogenetic') {
             healing = perSecond * (actual > 2 ? 1.5 : 1);
             healing = perSecond * (actual >= 2 ? 1.5 : 1);
             result.push(doseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. механического урона в секунду'));
             result.push(originalDoseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. механического урона в секунду'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду',
                 'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду',
                 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. ожогового и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
                 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. ожогового и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
             return result;
             return result;
         }
         }
         if (type === 'Anticorrosive') {
         if (type === 'Anticorrosive') {
             healing = perSecond * (actual > 2 ? 1.5 : 1);
             healing = perSecond * (actual >= 2 ? 1.5 : 1);
             result.push(doseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. термического урона в секунду'));
             result.push(originalDoseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. термического урона в секунду'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Наносит по ' + formatNumber(perSecond) + ' ед. тупого и токсического урона в секунду',
                 'Наносит по ' + formatNumber(perSecond) + ' ед. тупого и токсического урона в секунду',
                 'Дополнительно наносит по ' + formatNumber(perSecond * 5) + ' ед. тупого и токсического урона в секунду');
                 'Дополнительно наносит по ' + formatNumber(perSecond * 5) + ' ед. тупого и токсического урона в секунду');
             return result;
             return result;
         }
         }
         if (type === 'Biocidic') {
         if (type === 'Biocidic') {
             result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. механического урона в секунду'));
             result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. механического урона в секунду'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Наносит ' + formatNumber(perSecond * 2) + ' ед. механического урона в секунду',
                 'Наносит ' + formatNumber(perSecond * 2) + ' ед. механического урона в секунду',
                 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. механического урона в секунду');
                 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. механического урона в секунду');
             return result;
             return result;
         }
         }
         if (type === 'Antitoxic') {
         if (type === 'Antitoxic') {
             result.push(doseDescription('normal', 'Лечит по ' + formatNumber(perSecond * 2) + ' ед. токсического и генетического урона в секунду; удаляет 0,125 ед. токсичных веществ из крови в секунду'));
             result.push(originalDoseDescription('normal', 'Лечит по ' + formatNumber(perSecond * 2) + ' ед. токсического и генетического урона в секунду; удаляет 0,125 ед. токсичных веществ из крови в секунду'));
             addDoseEffects(result, reagent, '', 'Дополнительно даёт 5% шанс потерять сознание на 10 секунд');
             originalAddDoseEffects(result, reagent, '', 'Дополнительно даёт 5% шанс потерять сознание на 10 секунд');
             return result;
             return result;
         }
         }
         if (type === 'Oxygenating') {
         if (type === 'Oxygenating') {
             result.push(doseDescription('normal', actual >= 3
             result.push(originalDoseDescription('normal', actual >= 3 ?
                 ? 'Полностью лечит урон от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду'
                 'Полностью лечит урон от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду' :
                 : 'Лечит ' + formatNumber(perSecond) + ' ед. урона от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду'));
                 'Лечит ' + formatNumber(perSecond) + ' ед. урона от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. токсического урона в секунду',
                 'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. токсического урона в секунду',
                 'Дополнительно наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
                 'Дополнительно наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
             return result;
             return result;
         }
         }
         if (type === 'Electrogenetic') {
         if (type === 'Electrogenetic') {
             result.push(doseDescription('normal', 'При дефибрилляции лечит по ' + formatNumber(potency * 10) + ' ед. механического, термического и токсического урона; расходует 1 ед. препарата'));
             result.push(originalDoseDescription('normal', 'При дефибрилляции лечит по ' + formatNumber(potency * 10) + ' ед. механического, термического и токсического урона; расходует 1 ед. препарата'));
             return result;
             return result;
         }
         }
         if (type === 'Antihallucinogenic') {
         if (type === 'Antihallucinogenic') {
             result.push(doseDescription('normal', 'Удаляет по 2,5 ед. Токсина «Разрушитель разума» и космических наркотиков из крови; сокращает галлюцинации на ' + formatNumber(perSecond * 10) + ' сек. каждую секунду'));
             result.push(originalDoseDescription('normal', 'Удаляет по 2,5 ед. Токсина «Разрушитель разума» и космических наркотиков из крови; сокращает галлюцинации на ' + formatNumber(perSecond * 10) + ' сек. каждую секунду'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
                 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
                 'Дополнительно наносит по ' + formatNumber(perSecond) + ' ед. тупого и ожогового урона, а также ' + formatNumber(perSecond * 3) + ' ед. токсического урона в секунду');
                 'Дополнительно наносит по ' + formatNumber(perSecond) + ' ед. тупого и ожогового урона, а также ' + formatNumber(perSecond * 3) + ' ед. токсического урона в секунду');
             return result;
             return result;
         }
         }
         if (type === 'Focusing') {
         if (type === 'Focusing') {
             result.push(doseDescription('normal', 'Удаляет ' + formatAmount(perSecond) + ' алкоголя из крови и сокращает опьянение, заикание и дрожь на ' + formatNumber(perSecond * 2) + ' сек. каждую секунду' + (actual >= 3 ? '; также снимает немоту и слепоту' : '')));
             result.push(originalDoseDescription('normal', 'Удаляет ' + formatAmount(perSecond) + ' алкоголя из крови и сокращает опьянение, заикание и дрожь на ' + formatNumber(perSecond * 2) + ' сек. каждую секунду' + (actual >= 3 ? '; также снимает немоту и слепоту' : '')));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
                 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
                 'Дополнительно наносит ' + formatNumber(perSecond * 3) + ' ед. токсического урона в секунду');
                 'Дополнительно наносит ' + formatNumber(perSecond * 3) + ' ед. токсического урона в секунду');
             return result;
             return result;
         }
         }
         if (type === 'Thermostabilizing') {
         if (type === 'Thermostabilizing') {
             result.push(doseDescription('normal', 'Возвращает температуру тела к норме, изменяя её примерно на ' + formatNumber(perSecond * 60) + ' K в секунду'));
             result.push(originalDoseDescription('normal', 'Возвращает температуру тела к норме, изменяя её примерно на ' + formatNumber(perSecond * 60) + ' K в секунду'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent, 'Вызывает потерю сознания на 40 секунд', 'Дополнительно даёт 5% шанс потерять сознание на 10 секунд');
                'Вызывает потерю сознания на 40 секунд',
                'Дополнительно даёт 5% шанс потерять сознание на 10 секунд');
             return result;
             return result;
         }
         }
         if (type === 'Hemogenic') {
         if (type === 'Hemogenic') {
             result.push(doseDescription('normal', 'Если персонаж не голоден, восстанавливает ' + formatNumber(perSecond) + ' сл крови и расходует ' + formatNumber(perSecond) + ' ед. питания в секунду'));
             result.push(originalDoseDescription('normal', 'Если персонаж не голоден, восстанавливает ' + formatNumber(perSecond) + ' сл крови и расходует ' + formatNumber(perSecond) + ' ед. питания в секунду'));
             if (actual > 3) {
             if (actual >= 3) {
                 result.push(doseDescription('normal', 'При избытке крови наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду, а также замедляет'));
                 result.push(originalDoseDescription('normal', 'При избытке крови наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду, а также замедляет'));
             }
             }
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
                 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
                 'Дополнительно расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду');
                 'Дополнительно расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду');
             return result;
             return result;
         }
         }
         if (type === 'Ketogenic') {
         if (type === 'Ketogenic') {
             result.push(doseDescription('normal', 'Расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду и ускоряет выведение алкоголя'));
             result.push(originalDoseDescription('normal', 'Расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду и ускоряет выведение алкоголя'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Дополнительно расходует ' + formatNumber(perSecond * 5) + ' ед. питания, наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду и даёт ' + formatNumber(actual * 2.5) + '% шанс рвоты',
                 'Дополнительно расходует ' + formatNumber(perSecond * 5) + ' ед. питания, наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду и даёт ' + formatNumber(actual * 2.5) + '% шанс рвоты',
                 'Вызывает потерю сознания на 40 секунд');
                 'Вызывает потерю сознания на 40 секунд');
             return result;
             return result;
         }
         }
         if (type === 'Nutritious') {
         if (type === 'Nutritious') {
             var nutrition = ((Number(value.nutFactor || 0) * Number(value.nutMetabolism || 0)) + potency) * actual;
             var nutrition = ((Number(value.nutFactor || 0) * Number(value.nutMetabolism || 0)) + potency) * actual;
             result.push(doseDescription('normal', 'Восстанавливает ' + formatNumber(nutrition) + ' ед. питания за цикл метаболизма'));
             result.push(originalDoseDescription('normal', 'Восстанавливает ' + formatNumber(nutrition) + ' ед. питания за цикл метаболизма'));
             return result;
             return result;
         }
         }
         if (type === 'Toxic') {
         if (type === 'Toxic') {
             result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду'));
             result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Наносит ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду',
                 'Наносит ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду',
                 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. токсического урона в секунду');
                 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. токсического урона в секунду');
             return result;
             return result;
         }
         }
         if (type === 'Corrosive') {
         if (type === 'Corrosive') {
             result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. кислотного урона в секунду'));
             result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. кислотного урона в секунду'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Наносит ' + formatNumber(perSecond * 2) + ' ед. кислотного урона в секунду',
                 'Наносит ' + formatNumber(perSecond * 2) + ' ед. кислотного урона в секунду',
                 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. кислотного урона в секунду');
                 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. кислотного урона в секунду');
             return result;
             return result;
         }
         }
         if (type === 'Hypoxemic') {
         if (type === 'Hypoxemic') {
             result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду и даёт 10% шанс судорожного вдоха'));
             result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду и даёт 10% шанс судорожного вдоха'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Наносит по ' + formatNumber(perSecond) + ' ед. тупого и токсического урона, а также ' + formatNumber(perSecond * 5) + ' ед. урона от кислородного голодания в секунду',
                 'Наносит по ' + formatNumber(perSecond) + ' ед. тупого и токсического урона, а также ' + formatNumber(perSecond * 5) + ' ед. урона от кислородного голодания в секунду',
                 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
                 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
             return result;
             return result;
         }
         }
         if (type === 'Carcinogenic') {
         if (type === 'Carcinogenic') {
             result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. клеточного урона в секунду'));
             result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. клеточного урона в секунду'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Наносит ' + formatNumber(perSecond * 2) + ' ед. клеточного урона в секунду',
                 'Наносит ' + formatNumber(perSecond * 2) + ' ед. клеточного урона в секунду',
                 'Дополнительно наносит ' + formatNumber(perSecond * 2) + ' ед. тупого урона в секунду');
                 'Дополнительно наносит ' + formatNumber(perSecond * 2) + ' ед. тупого урона в секунду');
             return result;
             return result;
         }
         }
         if (type === 'Oxidizing') {
         if (type === 'Oxidizing') {
             result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду; в огненной смеси повышает интенсивность, но уменьшает длительность и радиус'));
             result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду; в огненной смеси повышает интенсивность, но уменьшает длительность и радиус'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Наносит ' + formatNumber(perSecond * 2) + ' ед. ожогового урона в секунду',
                 'Наносит ' + formatNumber(perSecond * 2) + ' ед. ожогового урона в секунду',
                 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. ожогового урона в секунду');
                 'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. ожогового урона в секунду');
             return result;
             return result;
         }
         }
         if (type === 'Fueling') {
         if (type === 'Fueling') {
             result.push(doseDescription('normal', 'Повышает воспламеняемость тела на ' + formatNumber(perSecond) + ' ед. в секунду; в огненной смеси увеличивает длительность и радиус, но снижает интенсивность'));
             result.push(originalDoseDescription('normal', 'Повышает воспламеняемость тела на ' + formatNumber(perSecond) + ' ед. в секунду; в огненной смеси увеличивает длительность и радиус, но снижает интенсивность'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Повышает воспламеняемость тела на ' + formatNumber(perSecond * 2) + ' ед. в секунду',
                 'Повышает воспламеняемость тела на ' + formatNumber(perSecond * 2) + ' ед. в секунду',
                 'Поджигает тело с высокой интенсивностью');
                 'Поджигает тело с высокой интенсивностью');
             return result;
             return result;
         }
         }
         if (type === 'Flowing') {
         if (type === 'Flowing') {
             result.push(doseDescription('normal', 'Делает огненную смесь более текучей: увеличивает радиус, но снижает интенсивность и длительность горения'));
             result.push(originalDoseDescription('normal', 'Делает огненную смесь более текучей: увеличивает радиус, но снижает интенсивность и длительность горения'));
             return result;
             return result;
         }
         }
         if (type === 'Viscous') {
         if (type === 'Viscous') {
             result.push(doseDescription('normal', 'Замедляет движение на ' + formatNumber(perSecond) + ' сек. за каждый цикл и уменьшает радиус огненной смеси'));
             result.push(originalDoseDescription('normal', 'Замедляет движение на ' + formatNumber(perSecond) + ' сек. за каждый цикл и уменьшает радиус огненной смеси'));
             addDoseEffects(result, reagent,
             originalAddDoseEffects(result, reagent,
                 'Сильно замедляет движение на ' + formatNumber(perSecond * 2) + ' сек. за каждый цикл',
                 'Сильно замедляет движение на ' + formatNumber(perSecond * 2) + ' сек. за каждый цикл',
                 'Практически останавливает движение на ' + formatNumber(perSecond * 3) + ' сек. за каждый цикл');
                 'Практически останавливает движение на ' + formatNumber(perSecond * 3) + ' сек. за каждый цикл');
             return result;
             return result;
         }
         }
         return null;
         return null;
     }
     }


     function genericEffectDescription(effect, reagent) {
     function originalGenericEffectDescription(effect, reagent) {
         var type = tagName(effect && effect.yamlTag);
         var type = tagName(effect && effect.yamlTag);
         var value = effectValue(effect);
         var value = originalEffectValue(effect);
         var probability = value.probability;
         var probability = value.probability;
         var tier = effectTier(effect, reagent);
         var tier = originalEffectTier(effect, reagent);
         var conditions = remainingConditions(effect, reagent);
         var conditions = originalRemainingConditions(effect, reagent);
         var text = '';
         var text = '';
         if (probability === 0) {
         if (probability === 0) {
             return [];
             return [];
         }
         }
 
         var rmc = originalRmcEffectDescriptions(type, value, reagent);
         var rmc = rmcEffectDescriptions(type, value, reagent);
         if (rmc) {
         if (rmc) {
             return rmc;
             return rmc;
         }
         }
         if (type === 'HealthChange' || type === 'EqualHealthChange') {
         if (type === 'HealthChange' || type === 'EqualHealthChange') {
             text = damageChanges(value.damage, type === 'EqualHealthChange').join('; ');
             text = originalDamageChanges(value.damage, type === 'EqualHealthChange').join('; ');
         } else if (type === 'AdjustReagent' && value.reagent) {
         } else if (type === 'AdjustReagent' && value.reagent) {
             text = (value.amount >= 0 ? 'Добавляет ' : 'Удаляет ') + formatAmount(Math.abs(value.amount || 0)) + ' реагента «' + capitalizeFirst(reagentName(value.reagent)) + '» за цикл метаболизма';
             text = (value.amount >= 0 ? 'Добавляет ' : 'Удаляет ') + formatAmount(Math.abs(value.amount || 0)) + ' реагента «' + capitalizeFirst(reagentName(value.reagent)) + '» за цикл метаболизма';
Строка 595: Строка 956:
         } else if (type === 'GenericStatusEffect') {
         } else if (type === 'GenericStatusEffect') {
             var status = STATUS_LABELS[value.key] || value.key || 'неизвестный статус';
             var status = STATUS_LABELS[value.key] || value.key || 'неизвестный статус';
             text = value.type === 'Remove'
             text = value.type === 'Remove' ? 'Сокращает ' + status + ' на ' + formatNumber(value.time || 2) + ' сек.' : 'Вызывает ' + status + ' на ' + formatNumber(value.time || 2) + ' сек.';
                ? 'Сокращает ' + status + ' на ' + formatNumber(value.time || 2) + ' сек.'
                : 'Вызывает ' + status + ' на ' + formatNumber(value.time || 2) + ' сек.';
         } else if (type === 'ModifyStatusEffect') {
         } else if (type === 'ModifyStatusEffect') {
             var modifiedStatus = STATUS_LABELS[value.effectProto] || value.effectProto || 'неизвестный статус';
             var modifiedStatus = STATUS_LABELS[value.effectProto] || value.effectProto || 'неизвестный статус';
             text = value.type === 'Remove'
             text = value.type === 'Remove' ? 'Сокращает ' + modifiedStatus + ' на ' + formatNumber(value.time || 2) + ' сек.' : 'Вызывает ' + modifiedStatus + ' на ' + formatNumber(value.time || 2) + ' сек.';
                ? 'Сокращает ' + modifiedStatus + ' на ' + formatNumber(value.time || 2) + ' сек.'
                : 'Вызывает ' + modifiedStatus + ' на ' + formatNumber(value.time || 2) + ' сек.';
         } else if (type === 'Jitter') {
         } else if (type === 'Jitter') {
             text = 'Вызывает дрожь на ' + formatNumber(value.time || 2) + ' сек.';
             text = 'Вызывает дрожь на ' + formatNumber(value.time || 2) + ' сек.';
Строка 624: Строка 981:
             text = 'Покрывает цель горючим; интенсивность составляет ' + formatNumber(Number(value.multiplier || 1) * 100) + '% от стандартной';
             text = 'Покрывает цель горючим; интенсивность составляет ' + formatNumber(Number(value.multiplier || 1) * 100) + '% от стандартной';
         } else if (type === 'PopupMessage') {
         } else if (type === 'PopupMessage') {
             text = value.messages && value.messages.indexOf('rmc-body-stings') !== -1
             text = value.messages && value.messages.indexOf('rmc-body-stings') !== -1 ? 'Вызывает болезненное жжение' : 'Вызывает заметное субъективное ощущение действия препарата';
                ? 'Вызывает болезненное жжение'
                : 'Вызывает заметное субъективное ощущение действия препарата';
         } else {
         } else {
             text = 'Дополнительный игровой эффект «' + (type || 'неизвестно') + '»';
             text = 'Дополнительный игровой эффект «' + (type || 'неизвестно') + '»';
         }
         }
         if (conditions.length) {
         if (conditions.length) {
             text += ' (' + conditions.join(', ') + ')';
             text += ' (' + conditions.join(', ') + ')';
         }
         }
 
         return [originalDoseDescription(tier, originalWithChance(text, probability))];
         return [doseDescription(tier, withChance(text, probability))];
     }
     }


     function semanticTokenClass(token) {
     function originalSemanticTokenClass(token) {
         var normalized = String(token || '').toLocaleLowerCase('ru-RU');
         var normalized = String(token || '').toLocaleLowerCase('ru-RU');
         var actionClass = EFFECT_ACTION_CLASSES[normalized];
         var actionClass = EFFECT_ACTION_CLASSES[normalized];
         if (actionClass) {
         if (actionClass) {
             return 'scmc-chem__effect-action scmc-chem__effect-action--' + actionClass;
             return 'scmc-chem__effect-action scmc-chem__effect-action--' + actionClass;
         }
         }
         for (var index = 0; index < DAMAGE_TOKEN_CLASSES.length; index += 1) {
         for (var index = 0; index < DAMAGE_TOKEN_CLASSES.length; index += 1) {
            DAMAGE_TOKEN_CLASSES[index].pattern.lastIndex = 0;
             if (DAMAGE_TOKEN_CLASSES[index].pattern.test(normalized)) {
             if (DAMAGE_TOKEN_CLASSES[index].pattern.test(normalized)) {
                 return 'scmc-chem__damage-type scmc-chem__damage-type--' + DAMAGE_TOKEN_CLASSES[index].className;
                 return 'scmc-chem__damage-type scmc-chem__damage-type--' + DAMAGE_TOKEN_CLASSES[index].className;
             }
             }
         }
         }
         return '';
         return '';
     }
     }


     function effectTone(text) {
     function originalEffectTone(text) {
         var value = String(text || '').toLocaleLowerCase('ru-RU');
         var value = String(text || '').toLocaleLowerCase('ru-RU');
         var beneficial = value.search(/полностью лечит|лечит|восстанавливает|возвращает|омолаживает|сокращает|удаляет|создаёт иммунитет/);
         var beneficial = value.search(/полностью лечит|лечит|восстанавливает|возвращает|омолаживает|сокращает|удаляет|создаёт иммунитет/);
Строка 661: Строка 1012:
         var warning = value.search(/расходует|замедляет/);
         var warning = value.search(/расходует|замедляет/);
         var candidates = [];
         var candidates = [];
 
         if (beneficial !== -1) { candidates.push({ index: beneficial, tone: 'beneficial' }); }
         if (beneficial !== -1) {
         if (harmful !== -1) { candidates.push({ index: harmful, tone: 'harmful' }); }
            candidates.push({ index: beneficial, tone: 'beneficial' });
         if (warning !== -1) { candidates.push({ index: warning, tone: 'warning' }); }
        }
         candidates.sort(function (left, right) { return left.index - right.index; });
         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 candidates.length ? candidates[0].tone : 'utility';
         return candidates.length ? candidates[0].tone : 'utility';
     }
     }


     function appendSemanticEffectText(parent, text) {
     function originalAppendSemanticEffectText(parent, text) {
         var value = finishSentence(text);
         var value = finishSentence(text);
         var lastIndex = 0;
         var lastIndex = 0;
         var match;
         var match;
         EFFECT_TOKEN_RE.lastIndex = 0;
         EFFECT_TOKEN_RE.lastIndex = 0;
         while ((match = EFFECT_TOKEN_RE.exec(value)) !== null) {
         while ((match = EFFECT_TOKEN_RE.exec(value)) !== null) {
Строка 689: Строка 1028:
                 parent.appendChild(document.createTextNode(value.slice(lastIndex, match.index)));
                 parent.appendChild(document.createTextNode(value.slice(lastIndex, match.index)));
             }
             }
 
             var className = originalSemanticTokenClass(match[0]);
             var className = semanticTokenClass(match[0]);
             parent.appendChild(className ? el('span', className, match[0]) : document.createTextNode(match[0]));
             parent.appendChild(className
                ? element('span', className, match[0])
                : document.createTextNode(match[0]));
             lastIndex = EFFECT_TOKEN_RE.lastIndex;
             lastIndex = EFFECT_TOKEN_RE.lastIndex;
         }
         }
         if (lastIndex < value.length) {
         if (lastIndex < value.length) {
             parent.appendChild(document.createTextNode(value.slice(lastIndex)));
             parent.appendChild(document.createTextNode(value.slice(lastIndex)));
Строка 702: Строка 1037:
     }
     }


     function renderEffectText(text) {
     function originalRenderEffectText(text) {
         var tone = effectTone(text);
         var tone = originalEffectTone(text);
         var item = element('li', 'scmc-chem__effect scmc-chem__effect--' + tone);
         var item = el('li', 'scmc-chem__effect scmc-chem__effect--' + tone);
         var content = element('span', 'scmc-chem__effect-name');
         var content = el('span', 'scmc-chem__effect-name');
         appendSemanticEffectText(content, text);
         originalAppendSemanticEffectText(content, text);
         item.appendChild(content);
         item.appendChild(content);
         return item;
         return item;
     }
     }


     function tierTitle(tier, reagent) {
     function originalTierTitle(tier, reagent) {
         var properties = reagent.properties || {};
         var properties = reagent.properties || {};
         if (tier === 'overdose') {
         if (tier === 'overdose') {
             return 'Передозировка' + (properties.overdose !== undefined ? ' · от ' + formatAmount(properties.overdose) : '');
             return 'Передозировка' + (properties.overdose !== undefined ? ' · от ' + formatAmount(properties.overdose) : '');
Строка 720: Строка 1054:
             return 'Критическая передозировка' + (properties.criticalOverdose !== undefined ? ' · от ' + formatAmount(properties.criticalOverdose) : '');
             return 'Критическая передозировка' + (properties.criticalOverdose !== undefined ? ' · от ' + formatAmount(properties.criticalOverdose) : '');
         }
         }
         return 'Обычная доза';
         return 'Обычная доза';
     }
     }


     function appendEffectTier(group, tier, effects, reagent) {
     function originalAppendEffectTier(group, tier, effects, reagent) {
         if (!effects.length) {
         if (!effects.length) {
             return;
             return;
         }
         }
 
         var tierBlock = el('div', 'scmc-chem__effect-tier scmc-chem__effect-tier--' + tier);
         var tierBlock = element('div', 'scmc-chem__effect-tier scmc-chem__effect-tier--' + tier);
         tierBlock.appendChild(el('div', 'scmc-chem__effect-tier-title', originalTierTitle(tier, reagent)));
         tierBlock.appendChild(element('div', 'scmc-chem__effect-tier-title', tierTitle(tier, reagent)));
         var list = el('ul', 'scmc-chem__effect-list');
         var list = element('ul', 'scmc-chem__effect-list');
         effects.forEach(function (description) { list.appendChild(originalRenderEffectText(description.text)); });
         effects.forEach(function (description) {
            list.appendChild(renderEffectText(description.text));
        });
         tierBlock.appendChild(list);
         tierBlock.appendChild(list);
         group.appendChild(tierBlock);
         group.appendChild(tierBlock);
     }
     }


     function renderMetabolisms(reagent) {
     function renderOriginalMetabolisms(reagent) {
         var metabolisms = reagent.properties && reagent.properties.metabolisms;
         var metabolisms = reagent.properties && reagent.properties.metabolisms;
         if (!metabolisms || !Object.keys(metabolisms).length) {
         if (!metabolisms || !Object.keys(metabolisms).length) {
             return null;
             return null;
         }
         }
 
         var block = el('div', 'scmc-chem__card-section');
         var block = element('div', 'scmc-chem__card-section');
         var hasContent = false;
         var hasContent = false;
         block.appendChild(element('h4', 'scmc-chem__card-subtitle', 'Воздействие'));
         block.appendChild(el('h4', 'scmc-chem__card-subtitle', 'Воздействие'));
 
         Object.keys(metabolisms).forEach(function (metabolismId) {
         Object.keys(metabolisms).forEach(function (metabolismId) {
             var metabolism = metabolisms[metabolismId] || {};
             var metabolism = metabolisms[metabolismId] || {};
             var tiers = { normal: [], overdose: [], critical: [] };
             var tiers = { normal: [], overdose: [], critical: [] };
             (metabolism.effects || []).forEach(function (effect) {
             (metabolism.effects || []).forEach(function (effect) {
                 genericEffectDescription(effect, reagent).forEach(function (description) {
                 originalGenericEffectDescription(effect, reagent).forEach(function (description) {
                     tiers[description.tier].push(description);
                     tiers[description.tier].push(description);
                 });
                 });
             });
             });
             if (!tiers.normal.length && !tiers.overdose.length && !tiers.critical.length) {
             if (!tiers.normal.length && !tiers.overdose.length && !tiers.critical.length) {
                 return;
                 return;
             }
             }
 
             var group = el('div', 'scmc-chem__metabolism');
             var group = element('div', 'scmc-chem__metabolism');
             var title = METABOLISM_LABELS[metabolismId] || metabolismId;
             var title = METABOLISM_LABELS[metabolismId] || metabolismId;
             if (metabolism.metabolismRate !== undefined) {
             if (metabolism.metabolismRate !== undefined) {
                 title += ' · расход ' + formatAmount(metabolism.metabolismRate) + '/сек.';
                 title += ' · расход ' + formatAmount(metabolism.metabolismRate) + '/сек.';
             }
             }
 
             group.appendChild(el('div', 'scmc-chem__metabolism-title', title));
             group.appendChild(element('div', 'scmc-chem__metabolism-title', title));
             originalAppendEffectTier(group, 'normal', tiers.normal, reagent);
             appendEffectTier(group, 'normal', tiers.normal, reagent);
             originalAppendEffectTier(group, 'overdose', tiers.overdose, reagent);
             appendEffectTier(group, 'overdose', tiers.overdose, reagent);
             originalAppendEffectTier(group, 'critical', tiers.critical, reagent);
             appendEffectTier(group, 'critical', tiers.critical, reagent);
             block.appendChild(group);
             block.appendChild(group);
             hasContent = true;
             hasContent = true;
         });
         });
         return hasContent ? block : null;
         return hasContent ? block : null;
     }
     }


     function plantEffectText(effect) {
     function originalPlantEffectText(effect) {
         var type = tagName(effect && effect.yamlTag);
         var type = tagName(effect && effect.yamlTag);
         var value = effectValue(effect);
         var value = originalEffectValue(effect);
         var probability = value.probability;
         var probability = value.probability;
         var amount = Number(value.amount !== undefined ? value.amount : 1);
         var amount = Number(value.amount !== undefined ? value.amount : 1);
         var attribute = PLANT_ATTRIBUTE_LABELS[type];
         var attribute = PLANT_ATTRIBUTE_LABELS[type];
         var text;
         var text;
         if (probability === 0) {
         if (probability === 0) {
             return '';
             return '';
         }
         }
         if (attribute) {
         if (attribute) {
             if (type === 'PlantAdjustHealth') {
             if (type === 'PlantAdjustHealth') {
                 text = amount >= 0
                 text = amount >= 0 ? 'Восстанавливает здоровье растения на ' + formatNumber(amount) : 'Снижает здоровье растения на ' + formatNumber(Math.abs(amount));
                    ? 'Восстанавливает здоровье растения на ' + formatNumber(amount)
                    : 'Снижает здоровье растения на ' + formatNumber(Math.abs(amount));
             } else {
             } else {
                 text = (amount >= 0 ? 'Повышает ' : 'Снижает ') + attribute + ' на ' + formatNumber(Math.abs(amount));
                 text = (amount >= 0 ? 'Повышает ' : 'Снижает ') + attribute + ' на ' + formatNumber(Math.abs(amount));
Строка 815: Строка 1132:
             text = 'Дополнительный растительный эффект «' + (type || 'неизвестно') + '»';
             text = 'Дополнительный растительный эффект «' + (type || 'неизвестно') + '»';
         }
         }
 
         return originalWithChance(text, probability);
         return withChance(text, probability);
     }
     }


     function renderPlantMetabolism(reagent) {
     function renderOriginalPlantMetabolism(reagent) {
         var effects = reagent.properties && reagent.properties.plantMetabolism;
         var effects = reagent.properties && reagent.properties.plantMetabolism;
         if (!Array.isArray(effects) || !effects.length) {
         if (!Array.isArray(effects) || !effects.length) {
             return null;
             return null;
         }
         }
 
         var descriptions = effects.map(originalPlantEffectText).filter(Boolean);
         var descriptions = effects.map(plantEffectText).filter(Boolean);
         if (!descriptions.length) {
         if (!descriptions.length) {
             return null;
             return null;
         }
         }
 
         var block = el('div', 'scmc-chem__card-section');
         var block = element('div', 'scmc-chem__card-section');
         block.appendChild(el('h4', 'scmc-chem__card-subtitle', 'Метаболизм растений'));
         block.appendChild(element('h4', 'scmc-chem__card-subtitle', 'Метаболизм растений'));
         var group = el('div', 'scmc-chem__metabolism');
         var group = element('div', 'scmc-chem__metabolism');
         group.appendChild(el('div', 'scmc-chem__metabolism-title', '1 ед. реагента каждые 3 секунды (базово)'));
         group.appendChild(element('div', 'scmc-chem__metabolism-title', '1 ед. реагента каждые 3 секунды (базово)'));
         var list = el('ul', 'scmc-chem__effect-list');
         var list = element('ul', 'scmc-chem__effect-list');
         descriptions.forEach(function (description) { list.appendChild(originalRenderEffectText(description)); });
         descriptions.forEach(function (description) {
            list.appendChild(renderEffectText(description));
        });
         group.appendChild(list);
         group.appendChild(list);
         block.appendChild(group);
         block.appendChild(group);
         return block;
         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', capitalizeFirst(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)); });
            add(body, recipeSection);
         }
         }
 
         add(body, renderOriginalMetabolisms(reagent));
         append(body, renderDoseBadges(reagent), renderRecipes(entry.id), renderMetabolisms(reagent), renderPlantMetabolism(reagent));
        add(body, renderOriginalPlantMetabolism(reagent));
 
         add(details, summary, body);
         if (!body.childNodes.length) {
            body.appendChild(element('p', 'scmc-chem__empty', 'Для этого реагента нет дополнительных данных.'));
        }
 
        details.appendChild(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', capitalizeFirst(group.title)),
    function renderTabs(root) {
                el('span', 'scmc-chem__breadcrumbs', group.path.map(capitalizeFirst).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: Строка 1233:
     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();
    }
}());

Текущая версия от 17:03, 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 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 REAGENT_LABELS = {
        UncookedAnimalProteins: 'сырые животные белки'
    };
    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 capitalizeFirst(value) {
        var text = String(value || '').trim();
        return text ? text.charAt(0).toLocaleUpperCase('ru-RU') + text.slice(1) : text;
    }

    function lowerFirst(value) {
        var text = String(value || '');
        return text ? text.charAt(0).toLocaleLowerCase('ru-RU') + text.slice(1) : text;
    }

    function finishSentence(value) {
        var text = capitalizeFirst(String(value || '').trim());
        return !text || /[.!?]$/.test(text) ? text : text + '.';
    }

    function tagName(tag) {
        return String(tag || '').replace(/^!type:/, '');
    }

    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 reagentName(id) {
        var reagent = getReagent(id);
        return reagent && reagent.name ? reagent.name : REAGENT_LABELS[id] || id;
    }

    function getName(id) {
        var reagent = getReagent(id);
        return capitalizeFirst(reagent && reagent.name ? reagent.name : REAGENT_LABELS[id] || 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',
            maxage: 0,
            smaxage: 0
        }), 'text/css');
    }

    function loadCatalog() {
        window.ScmcChemCatalog = null;
        return mw.loader.getScript(mw.util.getUrl(CATALOG_PAGE, {
            action: 'raw',
            ctype: 'text/javascript',
            maxage: 0,
            smaxage: 0
        })).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', '', capitalizeFirst(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', capitalizeFirst(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 action(text, kind) {
        return el('span', 'scmc-chem__effect-action scmc-chem__effect-action--' + kind, text);
    }

    function damageType(type) {
        var className = DAMAGE_CLASSES[type] || 'physical';
        return el('span', 'scmc-chem__damage-type scmc-chem__damage-type--' + className, DAMAGE_LABELS[type] || type);
    }

    function line() {
        var node = el('div', 'scmc-chem__effect-details');
        Array.prototype.slice.call(arguments).forEach(function (part) {
            node.appendChild(part && part.nodeType ? part : document.createTextNode(String(part)));
        });
        return node;
    }

    function probabilityText(value) {
        return value === undefined ? '' : ' · вероятность ' + formatNumber(value * 100) + '%';
    }

    function damageEntries(value) {
        var damage = value && value.damage;
        var result = [];
        if (Array.isArray(damage)) {
            damage.forEach(function (item) {
                Object.keys(item || {}).forEach(function (type) {
                    result.push({ type: type, amount: item[type] });
                });
            });
        } else if (damage && typeof damage === 'object') {
            ['groups', 'types'].forEach(function (section) {
                Object.keys(damage[section] || {}).forEach(function (type) {
                    result.push({ type: type, amount: damage[section][type] });
                });
            });
            if (!damage.groups && !damage.types) {
                Object.keys(damage).forEach(function (type) {
                    if (typeof damage[type] === 'number') {
                        result.push({ type: type, amount: damage[type] });
                    }
                });
            }
        }
        return result;
    }

    function effectKind(type, value) {
        var beneficial = /^(Anticorrosive|Antihallucinogenic|Antitoxic|Biocidic|ChemHealEyeDamage|CureZombieInfection|Electrogenetic|Focusing|Hemogenic|Neogenetic|Nutritious|Oxygenating|SatiateHunger|SatiateThirst|Thermostabilizing)$/;
        var harmful = /^(Carcinogenic|CauseZombieInfection|ChemVomit|Corrosive|Fueling|Hypoxemic|Jitter|Ketogenic|Oxidizing|Toxic|Viscous)$/;
        if (type === 'HealthChange' || type === 'EqualHealthChange') {
            var damage = damageEntries(value);
            return damage.length && damage.every(function (item) { return item.amount <= 0; }) ? 'beneficial' :
                damage.length && damage.every(function (item) { return item.amount >= 0; }) ? 'harmful' : 'warning';
        }
        if (beneficial.test(type)) {
            return 'beneficial';
        }
        if (harmful.test(type)) {
            return 'harmful';
        }
        return 'warning';
    }

    function conditionText(condition, reagent, hideDoseCondition) {
        var type = effectType(condition);
        var value = condition.value || {};
        if (type === 'ReagentThreshold') {
            if (hideDoseCondition) {
                return '';
            }
            return 'при содержании ' + (value.reagent ? getName(value.reagent) + ' ' : '') +
                (value.min !== undefined ? 'от ' + formatAmount(value.min) : '') +
                (value.max !== undefined ? ' до ' + formatAmount(value.max) : '');
        }
        if (type === 'Temperature') {
            return 'при температуре ' +
                (value.min !== undefined ? 'от ' + formatNumber(value.min) + ' K' : '') +
                (value.max !== undefined ? 'до ' + formatNumber(value.max) + ' K' : '');
        }
        if (type === 'MobStateCondition') {
            var stateName = value.mobstate || value.state || 'особое';
            return 'если цель находится в состоянии: ' + (MOB_STATE_LABELS[stateName] || stateName);
        }
        if (type === 'OrganType') {
            var organ = value.type || value.organType || 'подходящего типа';
            return 'для организма ' + (ORGAN_LABELS[organ] || organ);
        }
        return 'при выполнении дополнительного условия';
    }

    function renderEffect(effect, reagent, tier) {
        var type = effectType(effect);
        var value = effect.value && typeof effect.value === 'object' ? effect.value : {};
        var kind = effectKind(type, value);
        var card = el('div', 'scmc-chem__effect scmc-chem__effect--' + kind);
        add(card, el('div', 'scmc-chem__effect-name', EFFECT_LABELS[type] || ('Эффект «' + type + '»')));

        var damage = damageEntries(value);
        if (damage.length) {
            damage.forEach(function (item) {
                var healing = item.amount < 0;
                var oxygenLike = /^(Airloss|Asphyxiation|Bloodloss)$/.test(item.type);
                add(card, oxygenLike ? line(
                    action(healing ? 'Лечит' : 'Наносит', healing ? 'beneficial' : 'harmful'),
                    ' ' + formatNumber(Math.abs(item.amount)) + ' ед. урона от ', damageType(item.type)
                ) : line(
                    action(healing ? 'Лечит' : 'Наносит', healing ? 'beneficial' : 'harmful'),
                    ' ' + formatNumber(Math.abs(item.amount)) + ' ед. ', damageType(item.type), ' урона'
                ));
            });
        } else if (/^(Antitoxic|Toxic)$/.test(type)) {
            add(card, line(action(type === 'Antitoxic' ? 'Лечит' : 'Наносит', type === 'Antitoxic' ? 'beneficial' : 'harmful'),
                ' ' + formatNumber(value.potency || 1) + ' ед. ', damageType('Toxin'), ' урона'));
        } else if (/^(Biocidic|Carcinogenic)$/.test(type)) {
            add(card, line(action(type === 'Biocidic' ? 'Лечит' : 'Наносит', type === 'Biocidic' ? 'beneficial' : 'harmful'),
                ' ' + formatNumber(value.potency || 1) + ' ед. ', damageType('Cellular'), ' урона'));
        } else if (/^(Neogenetic|Electrogenetic)$/.test(type)) {
            add(card, line(action('Лечит', 'beneficial'), ' ' + formatNumber(value.potency || 1) + ' ед. ', damageType(type === 'Neogenetic' ? 'Brute' : 'Genetic'), ' урона'));
        } else if (/^(Anticorrosive|Corrosive)$/.test(type)) {
            add(card, line(action(type === 'Anticorrosive' ? 'Лечит' : 'Наносит', type === 'Anticorrosive' ? 'beneficial' : 'harmful'),
                ' ' + formatNumber(value.potency || 1) + ' ед. ', damageType('Caustic'), ' урона'));
        } else if (/^(Oxygenating|Hypoxemic)$/.test(type)) {
            add(card, line(action(type === 'Oxygenating' ? 'Лечит' : 'Наносит', type === 'Oxygenating' ? 'beneficial' : 'harmful'),
                ' ' + formatNumber(value.potency || 1) + ' ед. урона от ', damageType('Airloss')));
        } else if (type === 'Oxidizing') {
            add(card, line(action('Наносит', 'harmful'), ' ' + formatNumber(value.potency || 1) + ' ед. ', damageType('Burn'), ' урона'));
        } else if (type === 'Hemogenic') {
            add(card, line(action('Восстанавливает', 'beneficial'), ' кровь · сила ' + formatNumber(value.potency || 1)));
        } else if (type === 'AdjustReagent') {
            var amount = Number(value.amount || 0);
            add(card, line(action(amount < 0 ? 'Удаляет' : 'Добавляет', amount < 0 ? 'beneficial' : 'utility'),
                ' ' + formatAmount(Math.abs(amount)) + ' реагента «' + getName(value.reagent) + '»'));
        } else if (type === 'GenericStatusEffect' || type === 'ModifyStatusEffect') {
            var status = value.component || value.key || value.effectProto || 'статусный эффект';
            add(card, line(action(value.type === 'Remove' ? 'Снимает' : 'Вызывает', value.type === 'Remove' ? 'beneficial' : 'harmful'),
                ' ' + (STATUS_LABELS[status] || status) + (value.time !== undefined ? ' на ' + formatNumber(value.time) + ' с' : '')));
        } else if (type === 'Emote') {
            add(card, line(action('Вызывает', 'warning'), ' ' + (EMOTE_LABELS[value.emote] || value.emote || 'непроизвольную реакцию') + probabilityText(value.probability)));
        } else if (type === 'Jitter') {
            add(card, line(action('Вызывает', 'harmful'), ' дрожь' + probabilityText(value.probability)));
        } else if (type === 'ChemVomit') {
            add(card, line(action('Вызывает', 'harmful'), ' рвоту' + probabilityText(value.probability)));
        } else if (type === 'SatiateThirst' || type === 'SatiateHunger') {
            add(card, line(action(type === 'SatiateThirst' ? 'Утоляет жажду' : 'Утоляет голод', 'beneficial'),
                value.factor !== undefined ? ' · коэффициент ' + formatNumber(value.factor) : ''));
        } else if (type === 'Drunk') {
            add(card, line(action('Опьяняет', 'warning'), value.boozePower !== undefined ? ' · сила ' + formatNumber(value.boozePower) : ''));
        } else if (type === 'MovespeedModifier') {
            add(card, line(action('Изменяет скорость', 'utility'),
                ' · ходьба ×' + formatNumber(value.walkSpeedModifier || 1) + ' · бег ×' + formatNumber(value.sprintSpeedModifier || 1)));
        } else if (type === 'CureZombieInfection' || type === 'CauseZombieInfection') {
            add(card, line(action(type === 'CureZombieInfection' ? 'Лечит' : 'Заражает', type === 'CureZombieInfection' ? 'beneficial' : 'harmful'),
                (type === 'CureZombieInfection' ? ' зомби-инфекцию' : ' зомби-инфекцией') + (value.innoculate ? ' и создаёт иммунитет' : '')));
        } else if (type === 'PopupMessage') {
            add(card, line(action('Показывает', 'utility'), ' игровое сообщение' + probabilityText(value.probability)));
        } else {
            var potency = value.potency !== undefined ? ' · сила ' + formatNumber(value.potency) : '';
            var multiplier = value.multiplier !== undefined ? ' · множитель ' + formatNumber(value.multiplier) : '';
            add(card, line(action(EFFECT_LABELS[type] || 'Воздействует', kind === 'beneficial' ? 'beneficial' : kind === 'harmful' ? 'harmful' : 'utility'), potency + multiplier + probabilityText(value.probability)));
        }

        (value.conditions || []).forEach(function (condition) {
            var text = conditionText(condition, reagent, tier !== 'normal' && effectType(condition) === 'ReagentThreshold');
            if (text) {
                add(card, el('div', 'scmc-chem__effect-condition', 'Условие: ' + text));
            }
        });
        return card;
    }

    function effectDose(effect, reagent) {
        var conditions = effect.value && effect.value.conditions || [];
        var mins = conditions.filter(function (condition) {
            var value = condition.value || {};
            return effectType(condition) === 'ReagentThreshold' &&
                (!value.reagent || value.reagent === reagent.id) && value.min !== undefined;
        }).map(function (condition) { return Number(condition.value.min); });
        return mins.length ? Math.max.apply(Math, mins) : null;
    }

    function effectTier(effect, reagent) {
        var dose = effectDose(effect, reagent);
        var properties = reagent.properties || {};
        if (dose !== null && properties.criticalOverdose !== undefined && dose >= properties.criticalOverdose) {
            return 'critical';
        }
        if (dose !== null && properties.overdose !== undefined && dose >= properties.overdose) {
            return 'overdose';
        }
        return 'normal';
    }

    function tierTitle(tier, reagent) {
        var properties = reagent.properties || {};
        if (tier === 'critical') {
            return 'Критическая передозировка' + (properties.criticalOverdose !== undefined ? ' · от ' + formatAmount(properties.criticalOverdose) : '');
        }
        if (tier === 'overdose') {
            return 'Передозировка' + (properties.overdose !== undefined ? ' · от ' + formatAmount(properties.overdose) : '');
        }
        return 'Обычная доза' + (properties.overdose !== undefined ? ' · до ' + formatAmount(properties.overdose) : '');
    }

    function renderEffects(effects, reagent) {
        var list = el('div', 'scmc-chem__effect-list');
        if (!effects || !effects.length) {
            add(list, el('p', 'scmc-chem__empty', 'Эффекты не указаны.'));
            return list;
        }
        var tiers = { normal: [], overdose: [], critical: [] };
        effects.forEach(function (effect) {
            tiers[effectTier(effect, reagent)].push(effect);
        });
        ['normal', 'overdose', 'critical'].forEach(function (tier) {
            if (!tiers[tier].length) {
                return;
            }
            var block = el('div', 'scmc-chem__effect-tier scmc-chem__effect-tier--' + tier);
            add(block, el('div', 'scmc-chem__effect-tier-title', tierTitle(tier, reagent)));
            tiers[tier].forEach(function (effect) {
                add(block, renderEffect(effect, reagent, tier));
            });
            add(list, block);
        });
        return list;
    }

    function plantEffectKind(type, amount) {
        if (/Mutation/.test(type)) {
            return 'warning';
        }
        var harmfulWhenPositive = /Toxins|Weeds|Pests/.test(type);
        return (harmfulWhenPositive ? amount <= 0 : amount >= 0) ? 'beneficial' : 'harmful';
    }

    function renderPlantEffects(effects) {
        var list = el('div', 'scmc-chem__effect-list');
        (effects || []).forEach(function (effect) {
            var type = effectType(effect);
            var value = effect.value || {};
            if (type === 'PlantCryoxadone') {
                var special = el('div', 'scmc-chem__effect scmc-chem__effect--beneficial');
                add(special, el('div', 'scmc-chem__effect-name', 'Криогенное восстановление'));
                add(special, line(action('Омолаживает', 'beneficial'), ' растение при низкой температуре'));
                add(list, special);
                return;
            }
            var amount = Number(value.amount || 0);
            var kind = plantEffectKind(type, amount);
            var card = el('div', 'scmc-chem__effect scmc-chem__effect--' + kind);
            add(card, el('div', 'scmc-chem__effect-name', capitalizeFirst(PLANT_LABELS[type] || ('Эффект «' + type + '»'))));
            add(card, line(
                action(amount >= 0 ? 'Повышает' : 'Снижает', kind === 'beneficial' ? 'beneficial' : kind === 'harmful' ? 'harmful' : 'warning'),
                ' ' + (PLANT_LABELS[type] || 'показатель') + ' на ' + formatNumber(Math.abs(amount)) +
                    probabilityText(value.probability)
            ));
            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, reagent));
            add(wrapper, block);
        });
        return wrapper;
    }

    function renderPlantMetabolism(reagent) {
        var effects = reagent.properties && reagent.properties.plantMetabolism;
        if (!Array.isArray(effects) || !effects.length) {
            return null;
        }
        var wrapper = el('div', 'scmc-chem__card-section');
        var block = el('div', 'scmc-chem__metabolism');
        add(wrapper, el('h4', 'scmc-chem__card-subtitle', 'Воздействие на растения'));
        add(block, el('div', 'scmc-chem__metabolism-title', 'Метаболизм растений'), renderPlantEffects(effects));
        add(wrapper, block);
        return wrapper;
    }

    /* Исходная реализация описаний из js.txt. */
    function originalEffectValue(effect) {
        var value = effect && effect.value;
        return value && typeof value === 'object' ? value : {};
    }

    function originalWithChance(text, probability) {
        if (typeof probability !== 'number' || probability >= 1) {
            return finishSentence(text);
        }
        return finishSentence('С шансом ' + formatNumber(probability * 100) + '% ' + lowerFirst(text));
    }

    function originalConditionText(condition) {
        var type = tagName(condition && condition.yamlTag);
        var value = originalEffectValue(condition);
        if (type === 'ReagentThreshold') {
            if (value.min !== undefined && value.max !== undefined) {
                return 'при количестве от ' + formatAmount(value.min) + ' до ' + formatAmount(value.max);
            }
            if (value.min !== undefined) {
                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 originalThresholdMinimum(effect) {
        var conditions = originalEffectValue(effect).conditions;
        var minimum = null;
        if (!Array.isArray(conditions)) {
            return minimum;
        }
        conditions.forEach(function (condition) {
            if (tagName(condition && condition.yamlTag) !== 'ReagentThreshold') {
                return;
            }
            var value = originalEffectValue(condition);
            if (typeof value.min === 'number' && (minimum === null || value.min > minimum)) {
                minimum = value.min;
            }
        });
        return minimum;
    }

    function originalEffectTier(effect, reagent) {
        var minimum = originalThresholdMinimum(effect);
        var properties = reagent.properties || {};
        if (minimum === null) {
            return 'normal';
        }
        if (typeof properties.criticalOverdose === 'number' && minimum >= properties.criticalOverdose) {
            return 'critical';
        }
        if (typeof properties.overdose === 'number' && minimum >= properties.overdose) {
            return 'overdose';
        }
        return 'normal';
    }

    function originalRemainingConditions(effect, reagent) {
        var conditions = originalEffectValue(effect).conditions;
        if (!Array.isArray(conditions)) {
            return [];
        }
        return conditions.map(function (condition) {
            if (tagName(condition && condition.yamlTag) === 'ReagentThreshold' && originalEffectTier(effect, reagent) !== 'normal') {
                return '';
            }
            return originalConditionText(condition);
        }).filter(Boolean);
    }

    function originalDamageChanges(damage, equalGroups) {
        var entries = [];
        if (equalGroups && Array.isArray(damage)) {
            damage.forEach(function (entry) {
                var key = Object.keys(entry || {})[0];
                if (key) {
                    entries.push({ id: key, amount: entry[key] });
                }
            });
        } else if (damage && typeof damage === 'object') {
            ['groups', 'types'].forEach(function (key) {
                var values = damage[key];
                if (!values || typeof values !== 'object') {
                    return;
                }
                Object.keys(values).forEach(function (damageId) {
                    entries.push({ id: damageId, amount: values[damageId] });
                });
            });
        }
        return entries.map(function (entry) {
            var actionText = entry.amount < 0 ? 'Лечит' : 'Наносит';
            return actionText + ' ' + formatNumber(Math.abs(entry.amount)) + ' ед. ' +
                (DAMAGE_LABELS[entry.id] || ('урона типа «' + entry.id + '»')) + ' в секунду';
        });
    }

    function originalDoseDescription(tier, text) {
        return { tier: tier, text: finishSentence(text) };
    }

    function originalAddDoseEffects(result, reagent, overdoseText, criticalText) {
        var properties = reagent.properties || {};
        if (properties.overdose !== undefined && overdoseText) {
            result.push(originalDoseDescription('overdose', overdoseText));
        }
        if (properties.criticalOverdose !== undefined && criticalText) {
            result.push(originalDoseDescription('critical', criticalText));
        }
    }

    function originalRmcEffectDescriptions(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(originalDoseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. механического урона в секунду'));
            originalAddDoseEffects(result, reagent,
                'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду',
                'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. ожогового и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
            return result;
        }
        if (type === 'Anticorrosive') {
            healing = perSecond * (actual >= 2 ? 1.5 : 1);
            result.push(originalDoseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. термического урона в секунду'));
            originalAddDoseEffects(result, reagent,
                'Наносит по ' + formatNumber(perSecond) + ' ед. тупого и токсического урона в секунду',
                'Дополнительно наносит по ' + formatNumber(perSecond * 5) + ' ед. тупого и токсического урона в секунду');
            return result;
        }
        if (type === 'Biocidic') {
            result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. механического урона в секунду'));
            originalAddDoseEffects(result, reagent,
                'Наносит ' + formatNumber(perSecond * 2) + ' ед. механического урона в секунду',
                'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. механического урона в секунду');
            return result;
        }
        if (type === 'Antitoxic') {
            result.push(originalDoseDescription('normal', 'Лечит по ' + formatNumber(perSecond * 2) + ' ед. токсического и генетического урона в секунду; удаляет 0,125 ед. токсичных веществ из крови в секунду'));
            originalAddDoseEffects(result, reagent, '', 'Дополнительно даёт 5% шанс потерять сознание на 10 секунд');
            return result;
        }
        if (type === 'Oxygenating') {
            result.push(originalDoseDescription('normal', actual >= 3 ?
                'Полностью лечит урон от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду' :
                'Лечит ' + formatNumber(perSecond) + ' ед. урона от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду'));
            originalAddDoseEffects(result, reagent,
                'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. токсического урона в секунду',
                'Дополнительно наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
            return result;
        }
        if (type === 'Electrogenetic') {
            result.push(originalDoseDescription('normal', 'При дефибрилляции лечит по ' + formatNumber(potency * 10) + ' ед. механического, термического и токсического урона; расходует 1 ед. препарата'));
            return result;
        }
        if (type === 'Antihallucinogenic') {
            result.push(originalDoseDescription('normal', 'Удаляет по 2,5 ед. Токсина «Разрушитель разума» и космических наркотиков из крови; сокращает галлюцинации на ' + formatNumber(perSecond * 10) + ' сек. каждую секунду'));
            originalAddDoseEffects(result, reagent,
                'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
                'Дополнительно наносит по ' + formatNumber(perSecond) + ' ед. тупого и ожогового урона, а также ' + formatNumber(perSecond * 3) + ' ед. токсического урона в секунду');
            return result;
        }
        if (type === 'Focusing') {
            result.push(originalDoseDescription('normal', 'Удаляет ' + formatAmount(perSecond) + ' алкоголя из крови и сокращает опьянение, заикание и дрожь на ' + formatNumber(perSecond * 2) + ' сек. каждую секунду' + (actual >= 3 ? '; также снимает немоту и слепоту' : '')));
            originalAddDoseEffects(result, reagent,
                'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
                'Дополнительно наносит ' + formatNumber(perSecond * 3) + ' ед. токсического урона в секунду');
            return result;
        }
        if (type === 'Thermostabilizing') {
            result.push(originalDoseDescription('normal', 'Возвращает температуру тела к норме, изменяя её примерно на ' + formatNumber(perSecond * 60) + ' K в секунду'));
            originalAddDoseEffects(result, reagent, 'Вызывает потерю сознания на 40 секунд', 'Дополнительно даёт 5% шанс потерять сознание на 10 секунд');
            return result;
        }
        if (type === 'Hemogenic') {
            result.push(originalDoseDescription('normal', 'Если персонаж не голоден, восстанавливает ' + formatNumber(perSecond) + ' сл крови и расходует ' + formatNumber(perSecond) + ' ед. питания в секунду'));
            if (actual >= 3) {
                result.push(originalDoseDescription('normal', 'При избытке крови наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду, а также замедляет'));
            }
            originalAddDoseEffects(result, reagent,
                'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
                'Дополнительно расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду');
            return result;
        }
        if (type === 'Ketogenic') {
            result.push(originalDoseDescription('normal', 'Расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду и ускоряет выведение алкоголя'));
            originalAddDoseEffects(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(originalDoseDescription('normal', 'Восстанавливает ' + formatNumber(nutrition) + ' ед. питания за цикл метаболизма'));
            return result;
        }
        if (type === 'Toxic') {
            result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду'));
            originalAddDoseEffects(result, reagent,
                'Наносит ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду',
                'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. токсического урона в секунду');
            return result;
        }
        if (type === 'Corrosive') {
            result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. кислотного урона в секунду'));
            originalAddDoseEffects(result, reagent,
                'Наносит ' + formatNumber(perSecond * 2) + ' ед. кислотного урона в секунду',
                'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. кислотного урона в секунду');
            return result;
        }
        if (type === 'Hypoxemic') {
            result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду и даёт 10% шанс судорожного вдоха'));
            originalAddDoseEffects(result, reagent,
                'Наносит по ' + formatNumber(perSecond) + ' ед. тупого и токсического урона, а также ' + formatNumber(perSecond * 5) + ' ед. урона от кислородного голодания в секунду',
                'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
            return result;
        }
        if (type === 'Carcinogenic') {
            result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. клеточного урона в секунду'));
            originalAddDoseEffects(result, reagent,
                'Наносит ' + formatNumber(perSecond * 2) + ' ед. клеточного урона в секунду',
                'Дополнительно наносит ' + formatNumber(perSecond * 2) + ' ед. тупого урона в секунду');
            return result;
        }
        if (type === 'Oxidizing') {
            result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду; в огненной смеси повышает интенсивность, но уменьшает длительность и радиус'));
            originalAddDoseEffects(result, reagent,
                'Наносит ' + formatNumber(perSecond * 2) + ' ед. ожогового урона в секунду',
                'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. ожогового урона в секунду');
            return result;
        }
        if (type === 'Fueling') {
            result.push(originalDoseDescription('normal', 'Повышает воспламеняемость тела на ' + formatNumber(perSecond) + ' ед. в секунду; в огненной смеси увеличивает длительность и радиус, но снижает интенсивность'));
            originalAddDoseEffects(result, reagent,
                'Повышает воспламеняемость тела на ' + formatNumber(perSecond * 2) + ' ед. в секунду',
                'Поджигает тело с высокой интенсивностью');
            return result;
        }
        if (type === 'Flowing') {
            result.push(originalDoseDescription('normal', 'Делает огненную смесь более текучей: увеличивает радиус, но снижает интенсивность и длительность горения'));
            return result;
        }
        if (type === 'Viscous') {
            result.push(originalDoseDescription('normal', 'Замедляет движение на ' + formatNumber(perSecond) + ' сек. за каждый цикл и уменьшает радиус огненной смеси'));
            originalAddDoseEffects(result, reagent,
                'Сильно замедляет движение на ' + formatNumber(perSecond * 2) + ' сек. за каждый цикл',
                'Практически останавливает движение на ' + formatNumber(perSecond * 3) + ' сек. за каждый цикл');
            return result;
        }
        return null;
    }

    function originalGenericEffectDescription(effect, reagent) {
        var type = tagName(effect && effect.yamlTag);
        var value = originalEffectValue(effect);
        var probability = value.probability;
        var tier = originalEffectTier(effect, reagent);
        var conditions = originalRemainingConditions(effect, reagent);
        var text = '';
        if (probability === 0) {
            return [];
        }
        var rmc = originalRmcEffectDescriptions(type, value, reagent);
        if (rmc) {
            return rmc;
        }
        if (type === 'HealthChange' || type === 'EqualHealthChange') {
            text = originalDamageChanges(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(', ') + ')';
        }
        return [originalDoseDescription(tier, originalWithChance(text, probability))];
    }

    function originalSemanticTokenClass(token) {
        var normalized = String(token || '').toLocaleLowerCase('ru-RU');
        var actionClass = EFFECT_ACTION_CLASSES[normalized];
        if (actionClass) {
            return 'scmc-chem__effect-action scmc-chem__effect-action--' + actionClass;
        }
        for (var index = 0; index < DAMAGE_TOKEN_CLASSES.length; index += 1) {
            DAMAGE_TOKEN_CLASSES[index].pattern.lastIndex = 0;
            if (DAMAGE_TOKEN_CLASSES[index].pattern.test(normalized)) {
                return 'scmc-chem__damage-type scmc-chem__damage-type--' + DAMAGE_TOKEN_CLASSES[index].className;
            }
        }
        return '';
    }

    function originalEffectTone(text) {
        var value = String(text || '').toLocaleLowerCase('ru-RU');
        var beneficial = value.search(/полностью лечит|лечит|восстанавливает|возвращает|омолаживает|сокращает|удаляет|создаёт иммунитет/);
        var harmful = value.search(/наносит|вызывает|заражает|поджигает|уничтожает/);
        var warning = value.search(/расходует|замедляет/);
        var candidates = [];
        if (beneficial !== -1) { candidates.push({ index: beneficial, tone: 'beneficial' }); }
        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 candidates.length ? candidates[0].tone : 'utility';
    }

    function originalAppendSemanticEffectText(parent, text) {
        var value = finishSentence(text);
        var lastIndex = 0;
        var match;
        EFFECT_TOKEN_RE.lastIndex = 0;
        while ((match = EFFECT_TOKEN_RE.exec(value)) !== null) {
            if (match.index > lastIndex) {
                parent.appendChild(document.createTextNode(value.slice(lastIndex, match.index)));
            }
            var className = originalSemanticTokenClass(match[0]);
            parent.appendChild(className ? el('span', className, match[0]) : document.createTextNode(match[0]));
            lastIndex = EFFECT_TOKEN_RE.lastIndex;
        }
        if (lastIndex < value.length) {
            parent.appendChild(document.createTextNode(value.slice(lastIndex)));
        }
    }

    function originalRenderEffectText(text) {
        var tone = originalEffectTone(text);
        var item = el('li', 'scmc-chem__effect scmc-chem__effect--' + tone);
        var content = el('span', 'scmc-chem__effect-name');
        originalAppendSemanticEffectText(content, text);
        item.appendChild(content);
        return item;
    }

    function originalTierTitle(tier, reagent) {
        var properties = reagent.properties || {};
        if (tier === 'overdose') {
            return 'Передозировка' + (properties.overdose !== undefined ? ' · от ' + formatAmount(properties.overdose) : '');
        }
        if (tier === 'critical') {
            return 'Критическая передозировка' + (properties.criticalOverdose !== undefined ? ' · от ' + formatAmount(properties.criticalOverdose) : '');
        }
        return 'Обычная доза';
    }

    function originalAppendEffectTier(group, tier, effects, reagent) {
        if (!effects.length) {
            return;
        }
        var tierBlock = el('div', 'scmc-chem__effect-tier scmc-chem__effect-tier--' + tier);
        tierBlock.appendChild(el('div', 'scmc-chem__effect-tier-title', originalTierTitle(tier, reagent)));
        var list = el('ul', 'scmc-chem__effect-list');
        effects.forEach(function (description) { list.appendChild(originalRenderEffectText(description.text)); });
        tierBlock.appendChild(list);
        group.appendChild(tierBlock);
    }

    function renderOriginalMetabolisms(reagent) {
        var metabolisms = reagent.properties && reagent.properties.metabolisms;
        if (!metabolisms || !Object.keys(metabolisms).length) {
            return null;
        }
        var block = el('div', 'scmc-chem__card-section');
        var hasContent = false;
        block.appendChild(el('h4', 'scmc-chem__card-subtitle', 'Воздействие'));
        Object.keys(metabolisms).forEach(function (metabolismId) {
            var metabolism = metabolisms[metabolismId] || {};
            var tiers = { normal: [], overdose: [], critical: [] };
            (metabolism.effects || []).forEach(function (effect) {
                originalGenericEffectDescription(effect, reagent).forEach(function (description) {
                    tiers[description.tier].push(description);
                });
            });
            if (!tiers.normal.length && !tiers.overdose.length && !tiers.critical.length) {
                return;
            }
            var group = el('div', 'scmc-chem__metabolism');
            var title = METABOLISM_LABELS[metabolismId] || metabolismId;
            if (metabolism.metabolismRate !== undefined) {
                title += ' · расход ' + formatAmount(metabolism.metabolismRate) + '/сек.';
            }
            group.appendChild(el('div', 'scmc-chem__metabolism-title', title));
            originalAppendEffectTier(group, 'normal', tiers.normal, reagent);
            originalAppendEffectTier(group, 'overdose', tiers.overdose, reagent);
            originalAppendEffectTier(group, 'critical', tiers.critical, reagent);
            block.appendChild(group);
            hasContent = true;
        });
        return hasContent ? block : null;
    }

    function originalPlantEffectText(effect) {
        var type = tagName(effect && effect.yamlTag);
        var value = originalEffectValue(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 originalWithChance(text, probability);
    }

    function renderOriginalPlantMetabolism(reagent) {
        var effects = reagent.properties && reagent.properties.plantMetabolism;
        if (!Array.isArray(effects) || !effects.length) {
            return null;
        }
        var descriptions = effects.map(originalPlantEffectText).filter(Boolean);
        if (!descriptions.length) {
            return null;
        }
        var block = el('div', 'scmc-chem__card-section');
        block.appendChild(el('h4', 'scmc-chem__card-subtitle', 'Метаболизм растений'));
        var group = el('div', 'scmc-chem__metabolism');
        group.appendChild(el('div', 'scmc-chem__metabolism-title', '1 ед. реагента каждые 3 секунды (базово)'));
        var list = el('ul', 'scmc-chem__effect-list');
        descriptions.forEach(function (description) { list.appendChild(originalRenderEffectText(description)); });
        group.appendChild(list);
        block.appendChild(group);
        return block;
    }

    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', capitalizeFirst(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, renderOriginalMetabolisms(reagent));
        add(body, renderOriginalPlantMetabolism(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', capitalizeFirst(group.title)),
                el('span', 'scmc-chem__breadcrumbs', group.path.map(capitalizeFirst).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));