MediaWiki:ScmcChemistry.js: различия между версиями
Страница интерфейса MediaWiki
Дополнительные действия
Defer (обсуждение | вклад) Новая страница: «ывфыв» |
Defer (обсуждение | вклад) Нет описания правки |
||
| (не показана 1 промежуточная версия этой же участницы) | |||
| Строка 1: | Строка 1: | ||
/* | |||
* Интерактивный каталог химии SSMC. | |||
* Страница: MC:Химия | |||
* Данные: DeferW/ssmc-wiki-data | |||
*/ | |||
(function () { | |||
'use strict'; | |||
var ROOT_ID = 'scmc-chemistry-app'; | |||
var DATA_URL = 'https://raw.githubusercontent.com/DeferW/ssmc-wiki-data/main/data/chemistry-catalog.json'; | |||
var NUMBER = new Intl.NumberFormat('ru-RU', { | |||
maximumFractionDigits: 3 | |||
}); | |||
var TAB_DEFINITIONS = [ | |||
{ id: 'ordnance', label: 'Боевая химия' }, | |||
{ id: 'medicine', label: 'Медицина' }, | |||
{ id: 'drinks', label: 'Напитки' }, | |||
{ id: 'unlisted', label: 'Прочее' } | |||
]; | |||
var DAMAGE_LABELS = { | |||
Airloss: 'урона от кислородного голодания', | |||
Asphyxiation: 'урона от кислородного голодания', | |||
Bloodloss: 'урона от кровопотери', | |||
Blunt: 'тупого урона', | |||
Brute: 'механического урона', | |||
Burn: 'термического урона', | |||
Caustic: 'кислотного урона', | |||
Cellular: 'клеточного урона', | |||
Cold: 'холодового урона', | |||
Genetic: 'генетического урона', | |||
Heat: 'ожогового урона', | |||
Piercing: 'колотого урона', | |||
Poison: 'токсического урона', | |||
Radiation: 'радиационного урона', | |||
Slash: 'режущего урона', | |||
Toxin: 'токсического урона' | |||
}; | |||
var STATUS_LABELS = { | |||
Jitter: 'дрожь', | |||
RMCUnconscious: 'потерю сознания', | |||
StatusEffectSeeingRainbow: 'галлюцинации', | |||
Unconscious: 'потерю сознания' | |||
}; | |||
var EMOTE_LABELS = { | |||
Gasp: 'судорожный вдох', | |||
Laugh: 'смех', | |||
Scream: 'крик', | |||
Yawn: 'зевоту' | |||
}; | |||
var PLANT_ATTRIBUTE_LABELS = { | |||
PlantAdjustGrowth: 'возраст растения', | |||
PlantAdjustHealth: 'здоровье растения', | |||
PlantAdjustMutationLevel: 'уровень мутации', | |||
PlantAdjustMutationMod: 'модификатор мутации', | |||
PlantAdjustNutrition: 'уровень питания', | |||
PlantAdjustPests: 'уровень вредителей', | |||
PlantAdjustPotency: 'потенцию растения', | |||
PlantAdjustToxins: 'уровень токсинов', | |||
PlantAdjustWater: 'уровень воды', | |||
PlantAdjustWeeds: 'уровень сорняков', | |||
PlantAffectGrowth: 'возраст растения' | |||
}; | |||
var METABOLISM_LABELS = { | |||
Alcohol: 'Алкоголь', | |||
Drink: 'Напиток', | |||
Food: 'Пища', | |||
Medicine: 'Лекарство', | |||
Narcotic: 'Наркотик', | |||
Poison: 'Яд' | |||
}; | |||
var state = { | |||
data: null, | |||
activeTab: 'ordnance', | |||
query: '', | |||
productReactions: Object.create(null) | |||
}; | |||
function element(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 append(parent) { | |||
var children = Array.prototype.slice.call(arguments, 1); | |||
children.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 capitalizeFirst(value) { | |||
var text = String(value || ''); | |||
if (!text) { | |||
return text; | |||
} | |||
return text.charAt(0).toLocaleUpperCase('ru-RU') + text.slice(1); | |||
} | |||
function finishSentence(value) { | |||
var text = capitalizeFirst(String(value || '').trim()); | |||
if (!text || /[.!?]$/.test(text)) { | |||
return text; | |||
} | |||
return text + '.'; | |||
} | |||
function safeColor(value) { | |||
if (typeof value !== 'string') { | |||
return '#7f8c8d'; | |||
} | |||
if (/^#[0-9a-f]{3,8}$/i.test(value) || /^[a-z]+$/i.test(value)) { | |||
return value; | |||
} | |||
return '#7f8c8d'; | |||
} | |||
function tagName(tag) { | |||
return String(tag || '').replace(/^!type:/, ''); | |||
} | |||
function reagentById(id) { | |||
return state.data.reagents[id] || state.data.dependencies[id] || null; | |||
} | |||
function reagentName(id) { | |||
var reagent = reagentById(id); | |||
return reagent && reagent.name ? reagent.name : id; | |||
} | |||
function normalizeSearch(value) { | |||
return String(value || '').trim().toLocaleLowerCase('ru-RU'); | |||
} | |||
function matchesQuery(entry, reagent) { | |||
if (!state.query) { | |||
return true; | |||
} | |||
return normalizeSearch([ | |||
entry.id, | |||
entry.name, | |||
reagent && reagent.name, | |||
reagent && reagent.description, | |||
entry.sectionPath && entry.sectionPath.join(' ') | |||
].filter(Boolean).join(' ')).indexOf(state.query) !== -1; | |||
} | |||
function buildProductReactionIndex() { | |||
state.productReactions = Object.create(null); | |||
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 entriesForTab(tabId) { | |||
if (tabId === 'unlisted') { | |||
return (state.data.unlistedReagents || []).map(function (id) { | |||
var reagent = state.data.reagents[id]; | |||
return { | |||
id: id, | |||
name: reagent ? reagent.name : id, | |||
origin: reagent ? reagent.origin : 'unknown', | |||
sectionPath: ['Вне игровых руководств'], | |||
includedBy: 'catalog' | |||
}; | |||
}); | |||
} | |||
return state.data.guides[tabId] || []; | |||
} | |||
function effectValue(effect) { | |||
var value = effect && effect.value; | |||
return value && typeof value === 'object' ? value : {}; | |||
} | |||
function lowerFirst(value) { | |||
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) { | |||
return finishSentence(text); | |||
} | |||
return finishSentence('С шансом ' + formatNumber(probability * 100) + '% ' + lowerFirst(text)); | |||
} | |||
function conditionText(condition) { | |||
var type = tagName(condition && condition.yamlTag); | |||
var value = effectValue(condition); | |||
if (type === 'ReagentThreshold') { | |||
if (value.min !== undefined && value.max !== undefined) { | |||
return 'при количестве от ' + formatAmount(value.min) + ' до ' + formatAmount(value.max); | |||
} | |||
if (value.min !== undefined) { | |||
return 'при количестве от ' + formatAmount(value.min) + ' в организме'; | |||
} | |||
if (value.max !== undefined) { | |||
return 'при количестве до ' + formatAmount(value.max) + ' в организме'; | |||
} | |||
} | |||
if (type === 'Temperature') { | |||
if (value.min !== undefined && value.max !== undefined) { | |||
return 'при температуре от ' + formatNumber(value.min) + ' до ' + formatNumber(value.max) + ' K'; | |||
} | |||
if (value.min !== undefined) { | |||
return 'при температуре не ниже ' + formatNumber(value.min) + ' K'; | |||
} | |||
if (value.max !== undefined) { | |||
return 'при температуре не выше ' + formatNumber(value.max) + ' K'; | |||
} | |||
} | |||
if (type === 'MobStateCondition') { | |||
return value.mobstate === 'Critical' ? 'в критическом состоянии' : 'в состоянии «' + (value.mobstate || 'неизвестно') + '»'; | |||
} | |||
if (type === 'OrganType') { | |||
return (value.shouldHave === false ? 'кроме организмов типа ' : 'для организмов типа ') + (value.type || 'неизвестно'); | |||
} | |||
return type ? 'при выполнении условия «' + type + '»' : ''; | |||
} | |||
function thresholdMinimum(effect) { | |||
var conditions = effectValue(effect).conditions; | |||
var minimum = null; | |||
if (!Array.isArray(conditions)) { | |||
return minimum; | |||
} | |||
conditions.forEach(function (condition) { | |||
if (tagName(condition && condition.yamlTag) !== 'ReagentThreshold') { | |||
return; | |||
} | |||
var value = effectValue(condition); | |||
if (typeof value.min === 'number' && (minimum === null || value.min > minimum)) { | |||
minimum = value.min; | |||
} | |||
}); | |||
return minimum; | |||
} | |||
function effectTier(effect, reagent) { | |||
var minimum = thresholdMinimum(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 remainingConditions(effect, reagent) { | |||
var conditions = effectValue(effect).conditions; | |||
if (!Array.isArray(conditions)) { | |||
return []; | |||
} | |||
return conditions.map(function (condition) { | |||
if (tagName(condition && condition.yamlTag) === 'ReagentThreshold' && effectTier(effect, reagent) !== 'normal') { | |||
return ''; | |||
} | |||
return conditionText(condition); | |||
}).filter(Boolean); | |||
} | |||
function damageChanges(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 action = entry.amount < 0 ? 'Лечит' : 'Наносит'; | |||
return action + ' ' + formatNumber(Math.abs(entry.amount)) + ' ед. ' + (DAMAGE_LABELS[entry.id] || ('урона типа «' + entry.id + '»')) + ' в секунду'; | |||
}); | |||
} | |||
function doseDescription(tier, text) { | |||
return { tier: tier, text: finishSentence(text) }; | |||
} | |||
function addDoseEffects(result, reagent, overdoseText, criticalText) { | |||
var properties = reagent.properties || {}; | |||
if (properties.overdose !== undefined && overdoseText) { | |||
result.push(doseDescription('overdose', overdoseText)); | |||
} | |||
if (properties.criticalOverdose !== undefined && criticalText) { | |||
result.push(doseDescription('critical', criticalText)); | |||
} | |||
} | |||
function rmcEffectDescriptions(type, value, reagent) { | |||
var result = []; | |||
var potency = Number(value.potency || 0); | |||
var actual = potency * 0.5; | |||
var perSecond = potency * 0.25; | |||
var healing; | |||
if (type === 'Neogenetic') { | |||
healing = perSecond * (actual > 2 ? 1.5 : 1); | |||
result.push(doseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. механического урона в секунду')); | |||
addDoseEffects(result, reagent, | |||
'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду', | |||
'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. ожогового и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду'); | |||
return result; | |||
} | |||
if (type === 'Anticorrosive') { | |||
healing = perSecond * (actual > 2 ? 1.5 : 1); | |||
result.push(doseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. термического урона в секунду')); | |||
addDoseEffects(result, reagent, | |||
'Наносит по ' + formatNumber(perSecond) + ' ед. тупого и токсического урона в секунду', | |||
'Дополнительно наносит по ' + formatNumber(perSecond * 5) + ' ед. тупого и токсического урона в секунду'); | |||
return result; | |||
} | |||
if (type === 'Biocidic') { | |||
result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. механического урона в секунду')); | |||
addDoseEffects(result, reagent, | |||
'Наносит ' + formatNumber(perSecond * 2) + ' ед. механического урона в секунду', | |||
'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. механического урона в секунду'); | |||
return result; | |||
} | |||
if (type === 'Antitoxic') { | |||
result.push(doseDescription('normal', 'Лечит по ' + formatNumber(perSecond * 2) + ' ед. токсического и генетического урона в секунду; удаляет 0,125 ед. токсичных веществ из крови в секунду')); | |||
addDoseEffects(result, reagent, '', 'Дополнительно даёт 5% шанс потерять сознание на 10 секунд'); | |||
return result; | |||
} | |||
if (type === 'Oxygenating') { | |||
result.push(doseDescription('normal', actual >= 3 | |||
? 'Полностью лечит урон от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду' | |||
: 'Лечит ' + formatNumber(perSecond) + ' ед. урона от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду')); | |||
addDoseEffects(result, reagent, | |||
'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. токсического урона в секунду', | |||
'Дополнительно наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду'); | |||
return result; | |||
} | |||
if (type === 'Electrogenetic') { | |||
result.push(doseDescription('normal', 'При дефибрилляции лечит по ' + formatNumber(potency * 10) + ' ед. механического, термического и токсического урона; расходует 1 ед. препарата')); | |||
return result; | |||
} | |||
if (type === 'Antihallucinogenic') { | |||
result.push(doseDescription('normal', 'Удаляет по 2,5 ед. Токсина «Разрушитель разума» и космических наркотиков из крови; сокращает галлюцинации на ' + formatNumber(perSecond * 10) + ' сек. каждую секунду')); | |||
addDoseEffects(result, reagent, | |||
'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду', | |||
'Дополнительно наносит по ' + formatNumber(perSecond) + ' ед. тупого и ожогового урона, а также ' + formatNumber(perSecond * 3) + ' ед. токсического урона в секунду'); | |||
return result; | |||
} | |||
if (type === 'Focusing') { | |||
result.push(doseDescription('normal', 'Удаляет ' + formatAmount(perSecond) + ' алкоголя из крови и сокращает опьянение, заикание и дрожь на ' + formatNumber(perSecond * 2) + ' сек. каждую секунду' + (actual >= 3 ? '; также снимает немоту и слепоту' : ''))); | |||
addDoseEffects(result, reagent, | |||
'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду', | |||
'Дополнительно наносит ' + formatNumber(perSecond * 3) + ' ед. токсического урона в секунду'); | |||
return result; | |||
} | |||
if (type === 'Thermostabilizing') { | |||
result.push(doseDescription('normal', 'Возвращает температуру тела к норме, изменяя её примерно на ' + formatNumber(perSecond * 60) + ' K в секунду')); | |||
addDoseEffects(result, reagent, | |||
'Вызывает потерю сознания на 40 секунд', | |||
'Дополнительно даёт 5% шанс потерять сознание на 10 секунд'); | |||
return result; | |||
} | |||
if (type === 'Hemogenic') { | |||
result.push(doseDescription('normal', 'Если персонаж не голоден, восстанавливает ' + formatNumber(perSecond) + ' сл крови и расходует ' + formatNumber(perSecond) + ' ед. питания в секунду')); | |||
if (actual > 3) { | |||
result.push(doseDescription('normal', 'При избытке крови наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду, а также замедляет')); | |||
} | |||
addDoseEffects(result, reagent, | |||
'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду', | |||
'Дополнительно расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду'); | |||
return result; | |||
} | |||
if (type === 'Ketogenic') { | |||
result.push(doseDescription('normal', 'Расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду и ускоряет выведение алкоголя')); | |||
addDoseEffects(result, reagent, | |||
'Дополнительно расходует ' + formatNumber(perSecond * 5) + ' ед. питания, наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду и даёт ' + formatNumber(actual * 2.5) + '% шанс рвоты', | |||
'Вызывает потерю сознания на 40 секунд'); | |||
return result; | |||
} | |||
if (type === 'Nutritious') { | |||
var nutrition = ((Number(value.nutFactor || 0) * Number(value.nutMetabolism || 0)) + potency) * actual; | |||
result.push(doseDescription('normal', 'Восстанавливает ' + formatNumber(nutrition) + ' ед. питания за цикл метаболизма')); | |||
return result; | |||
} | |||
if (type === 'Toxic') { | |||
result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду')); | |||
addDoseEffects(result, reagent, | |||
'Наносит ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду', | |||
'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. токсического урона в секунду'); | |||
return result; | |||
} | |||
if (type === 'Corrosive') { | |||
result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. кислотного урона в секунду')); | |||
addDoseEffects(result, reagent, | |||
'Наносит ' + formatNumber(perSecond * 2) + ' ед. кислотного урона в секунду', | |||
'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. кислотного урона в секунду'); | |||
return result; | |||
} | |||
if (type === 'Hypoxemic') { | |||
result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду и даёт 10% шанс судорожного вдоха')); | |||
addDoseEffects(result, reagent, | |||
'Наносит по ' + formatNumber(perSecond) + ' ед. тупого и токсического урона, а также ' + formatNumber(perSecond * 5) + ' ед. урона от кислородного голодания в секунду', | |||
'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду'); | |||
return result; | |||
} | |||
if (type === 'Carcinogenic') { | |||
result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. клеточного урона в секунду')); | |||
addDoseEffects(result, reagent, | |||
'Наносит ' + formatNumber(perSecond * 2) + ' ед. клеточного урона в секунду', | |||
'Дополнительно наносит ' + formatNumber(perSecond * 2) + ' ед. тупого урона в секунду'); | |||
return result; | |||
} | |||
if (type === 'Oxidizing') { | |||
result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду; в огненной смеси повышает интенсивность, но уменьшает длительность и радиус')); | |||
addDoseEffects(result, reagent, | |||
'Наносит ' + formatNumber(perSecond * 2) + ' ед. ожогового урона в секунду', | |||
'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. ожогового урона в секунду'); | |||
return result; | |||
} | |||
if (type === 'Fueling') { | |||
result.push(doseDescription('normal', 'Повышает воспламеняемость тела на ' + formatNumber(perSecond) + ' ед. в секунду; в огненной смеси увеличивает длительность и радиус, но снижает интенсивность')); | |||
addDoseEffects(result, reagent, | |||
'Повышает воспламеняемость тела на ' + formatNumber(perSecond * 2) + ' ед. в секунду', | |||
'Поджигает тело с высокой интенсивностью'); | |||
return result; | |||
} | |||
if (type === 'Flowing') { | |||
result.push(doseDescription('normal', 'Делает огненную смесь более текучей: увеличивает радиус, но снижает интенсивность и длительность горения')); | |||
return result; | |||
} | |||
if (type === 'Viscous') { | |||
result.push(doseDescription('normal', 'Замедляет движение на ' + formatNumber(perSecond) + ' сек. за каждый цикл и уменьшает радиус огненной смеси')); | |||
addDoseEffects(result, reagent, | |||
'Сильно замедляет движение на ' + formatNumber(perSecond * 2) + ' сек. за каждый цикл', | |||
'Практически останавливает движение на ' + formatNumber(perSecond * 3) + ' сек. за каждый цикл'); | |||
return result; | |||
} | |||
return null; | |||
} | |||
function genericEffectDescription(effect, reagent) { | |||
var type = tagName(effect && effect.yamlTag); | |||
var value = effectValue(effect); | |||
var probability = value.probability; | |||
var tier = effectTier(effect, reagent); | |||
var conditions = remainingConditions(effect, reagent); | |||
var text = ''; | |||
if (probability === 0) { | |||
return []; | |||
} | |||
var rmc = rmcEffectDescriptions(type, value, reagent); | |||
if (rmc) { | |||
return rmc; | |||
} | |||
if (type === 'HealthChange' || type === 'EqualHealthChange') { | |||
text = damageChanges(value.damage, type === 'EqualHealthChange').join('; '); | |||
} else if (type === 'AdjustReagent' && value.reagent) { | |||
text = (value.amount >= 0 ? 'Добавляет ' : 'Удаляет ') + formatAmount(Math.abs(value.amount || 0)) + ' реагента «' + capitalizeFirst(reagentName(value.reagent)) + '» за цикл метаболизма'; | |||
} else if (type === 'MovespeedModifier') { | |||
var walk = Math.round((Number(value.walkSpeedModifier || 1) - 1) * 100); | |||
var sprint = Math.round((Number(value.sprintSpeedModifier || 1) - 1) * 100); | |||
text = 'Изменяет скорость ходьбы на ' + (walk >= 0 ? '+' : '') + walk + '% и бега на ' + (sprint >= 0 ? '+' : '') + sprint + '%'; | |||
} else if (type === 'GenericStatusEffect') { | |||
var status = STATUS_LABELS[value.key] || value.key || 'неизвестный статус'; | |||
text = value.type === 'Remove' | |||
? 'Сокращает ' + status + ' на ' + formatNumber(value.time || 2) + ' сек.' | |||
: 'Вызывает ' + status + ' на ' + formatNumber(value.time || 2) + ' сек.'; | |||
} else if (type === 'ModifyStatusEffect') { | |||
var modifiedStatus = STATUS_LABELS[value.effectProto] || value.effectProto || 'неизвестный статус'; | |||
text = value.type === 'Remove' | |||
? 'Сокращает ' + modifiedStatus + ' на ' + formatNumber(value.time || 2) + ' сек.' | |||
: 'Вызывает ' + modifiedStatus + ' на ' + formatNumber(value.time || 2) + ' сек.'; | |||
} else if (type === 'Jitter') { | |||
text = 'Вызывает дрожь на ' + formatNumber(value.time || 2) + ' сек.'; | |||
} else if (type === 'Emote') { | |||
text = 'Вызывает ' + (EMOTE_LABELS[value.emote] || ('эмоцию «' + (value.emote || 'неизвестно') + '»')); | |||
} else if (type === 'ChemVomit') { | |||
text = 'Вызывает рвоту'; | |||
} else if (type === 'Drunk') { | |||
text = 'Добавляет ' + formatNumber(value.boozePower || 3) + ' сек. опьянения за цикл метаболизма'; | |||
} else if (type === 'SatiateHunger') { | |||
text = 'Восстанавливает ' + formatNumber(value.factor !== undefined ? value.factor : 3) + ' ед. насыщения за цикл метаболизма'; | |||
} else if (type === 'SatiateThirst') { | |||
text = 'Восстанавливает ' + formatNumber(value.factor !== undefined ? value.factor : 3) + ' ед. жажды за цикл метаболизма'; | |||
} else if (type === 'ChemHealEyeDamage') { | |||
text = 'Лечит повреждения глаз'; | |||
} else if (type === 'CauseZombieInfection') { | |||
text = 'Заражает зомби-инфекцией'; | |||
} else if (type === 'CureZombieInfection') { | |||
text = value.innoculate ? 'Лечит зомби-инфекцию и создаёт иммунитет' : 'Лечит зомби-инфекцию'; | |||
} else if (type === 'FlammableReaction') { | |||
text = 'Покрывает цель горючим; интенсивность составляет ' + formatNumber(Number(value.multiplier || 1) * 100) + '% от стандартной'; | |||
} else if (type === 'PopupMessage') { | |||
text = value.messages && value.messages.indexOf('rmc-body-stings') !== -1 | |||
? 'Вызывает болезненное жжение' | |||
: 'Вызывает заметное субъективное ощущение действия препарата'; | |||
} else { | |||
text = 'Дополнительный игровой эффект «' + (type || 'неизвестно') + '»'; | |||
} | |||
if (conditions.length) { | |||
text += ' (' + conditions.join(', ') + ')'; | |||
} | |||
return [doseDescription(tier, withChance(text, probability))]; | |||
} | |||
function renderEffectText(text) { | |||
var item = element('li', 'scmc-chem__effect'); | |||
item.appendChild(element('span', 'scmc-chem__effect-name', finishSentence(text))); | |||
return item; | |||
} | |||
function tierTitle(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 appendEffectTier(group, tier, effects, reagent) { | |||
if (!effects.length) { | |||
return; | |||
} | |||
var tierBlock = element('div', 'scmc-chem__effect-tier scmc-chem__effect-tier--' + tier); | |||
tierBlock.appendChild(element('div', 'scmc-chem__effect-tier-title', tierTitle(tier, reagent))); | |||
var list = element('ul', 'scmc-chem__effect-list'); | |||
effects.forEach(function (description) { | |||
list.appendChild(renderEffectText(description.text)); | |||
}); | |||
tierBlock.appendChild(list); | |||
group.appendChild(tierBlock); | |||
} | |||
function renderMetabolisms(reagent) { | |||
var metabolisms = reagent.properties && reagent.properties.metabolisms; | |||
if (!metabolisms || !Object.keys(metabolisms).length) { | |||
return null; | |||
} | |||
var block = element('div', 'scmc-chem__card-section'); | |||
var hasContent = false; | |||
block.appendChild(element('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) { | |||
genericEffectDescription(effect, reagent).forEach(function (description) { | |||
tiers[description.tier].push(description); | |||
}); | |||
}); | |||
if (!tiers.normal.length && !tiers.overdose.length && !tiers.critical.length) { | |||
return; | |||
} | |||
var group = element('div', 'scmc-chem__metabolism'); | |||
var title = METABOLISM_LABELS[metabolismId] || metabolismId; | |||
if (metabolism.metabolismRate !== undefined) { | |||
title += ' · расход ' + formatAmount(metabolism.metabolismRate) + '/сек.'; | |||
} | |||
group.appendChild(element('div', 'scmc-chem__metabolism-title', title)); | |||
appendEffectTier(group, 'normal', tiers.normal, reagent); | |||
appendEffectTier(group, 'overdose', tiers.overdose, reagent); | |||
appendEffectTier(group, 'critical', tiers.critical, reagent); | |||
block.appendChild(group); | |||
hasContent = true; | |||
}); | |||
return hasContent ? block : null; | |||
} | |||
function plantEffectText(effect) { | |||
var type = tagName(effect && effect.yamlTag); | |||
var value = effectValue(effect); | |||
var probability = value.probability; | |||
var amount = Number(value.amount !== undefined ? value.amount : 1); | |||
var attribute = PLANT_ATTRIBUTE_LABELS[type]; | |||
var text; | |||
if (probability === 0) { | |||
return ''; | |||
} | |||
if (attribute) { | |||
if (type === 'PlantAdjustHealth') { | |||
text = amount >= 0 | |||
? 'Восстанавливает здоровье растения на ' + formatNumber(amount) | |||
: 'Снижает здоровье растения на ' + formatNumber(Math.abs(amount)); | |||
} else { | |||
text = (amount >= 0 ? 'Повышает ' : 'Снижает ') + attribute + ' на ' + formatNumber(Math.abs(amount)); | |||
} | |||
} else if (type === 'PlantCryoxadone') { | |||
text = 'Омолаживает растение с учётом его возраста и времени роста'; | |||
} else if (type === 'PlantPhalanximine') { | |||
text = 'Возвращает жизнеспособность растению, погибшему из-за мутации'; | |||
} else if (type === 'PlantDiethylamine') { | |||
text = 'Повышает продолжительность жизни или базовое здоровье растения; шанс 10% на единицу реагента'; | |||
} else if (type === 'PlantRestoreSeeds') { | |||
text = 'Восстанавливает семена растения'; | |||
} else if (type === 'PlantDestroySeeds') { | |||
text = 'Уничтожает семена растения'; | |||
} else { | |||
text = 'Дополнительный растительный эффект «' + (type || 'неизвестно') + '»'; | |||
} | |||
return withChance(text, probability); | |||
} | |||
function renderPlantMetabolism(reagent) { | |||
var effects = reagent.properties && reagent.properties.plantMetabolism; | |||
if (!Array.isArray(effects) || !effects.length) { | |||
return null; | |||
} | |||
var descriptions = effects.map(plantEffectText).filter(Boolean); | |||
if (!descriptions.length) { | |||
return null; | |||
} | |||
var block = element('div', 'scmc-chem__card-section'); | |||
block.appendChild(element('h4', 'scmc-chem__card-subtitle', 'Метаболизм растений')); | |||
var group = element('div', 'scmc-chem__metabolism'); | |||
group.appendChild(element('div', 'scmc-chem__metabolism-title', '1 ед. реагента каждые 3 секунды (базово)')); | |||
var list = element('ul', 'scmc-chem__effect-list'); | |||
descriptions.forEach(function (description) { | |||
list.appendChild(renderEffectText(description)); | |||
}); | |||
group.appendChild(list); | |||
block.appendChild(group); | |||
return block; | |||
} | |||
function renderMixture(items, className) { | |||
var list = element('div', className); | |||
(items || []).forEach(function (item) { | |||
var row = element('div', 'scmc-chem__mixture-item'); | |||
var dot = element('span', 'scmc-chem__mixture-dot'); | |||
var reagent = reagentById(item.id); | |||
dot.style.backgroundColor = safeColor(reagent && reagent.properties && reagent.properties.color); | |||
append( | |||
row, | |||
dot, | |||
element('span', 'scmc-chem__mixture-name', capitalizeFirst(item.name || reagentName(item.id))), | |||
element('span', 'scmc-chem__mixture-amount', formatAmount(item.amount)) | |||
); | |||
list.appendChild(row); | |||
}); | |||
return list; | |||
} | |||
function reactionEffectText(effect) { | |||
var type = tagName(effect && effect.yamlTag); | |||
var value = effect && effect.value; | |||
value = value && typeof value === 'object' ? value : {}; | |||
if (type === 'SensitiveReactionExplosionEffect') { | |||
return 'Взрыв при объёме от ' + formatAmount(value.threshold || 0) + '; интенсивность за единицу: ' + formatNumber(value.intensityPerUnit || 0) + '.'; | |||
} | |||
if (type === 'ExplosionReactionEffect') { | |||
return 'Вызывает взрыв; интенсивность за единицу: ' + formatNumber(value.intensityPerUnit || 0) + '.'; | |||
} | |||
if (type === 'AreaReactionEffect') { | |||
return 'Создаёт эффект в области' + (value.duration ? ' на ' + formatNumber(value.duration) + ' сек.' : '.'); | |||
} | |||
if (type === 'CreateEntityReactionEffect') { | |||
return 'Создаёт объект: ' + (value.entity || 'неизвестно') + '.'; | |||
} | |||
return type || 'Дополнительный эффект реакции'; | |||
} | |||
function renderReaction(reaction, index) { | |||
var block = element('div', 'scmc-chem__reaction'); | |||
if (index > 0) { | |||
block.appendChild(element('div', 'scmc-chem__reaction-label', 'Вариант ' + (index + 1))); | |||
} | |||
var equation = element('div', 'scmc-chem__equation'); | |||
append( | |||
equation, | |||
renderMixture(reaction.reactants, 'scmc-chem__reactants'), | |||
element('div', 'scmc-chem__arrow', '→'), | |||
renderMixture(reaction.products, 'scmc-chem__products') | |||
); | |||
block.appendChild(equation); | |||
if (reaction.conditions && reaction.conditions.minTemp !== undefined) { | |||
block.appendChild(element('div', 'scmc-chem__reaction-note', 'Минимальная температура: ' + formatNumber(reaction.conditions.minTemp) + ' K.')); | |||
} | |||
(reaction.effects || []).forEach(function (effect) { | |||
block.appendChild(element('div', 'scmc-chem__reaction-warning', reactionEffectText(effect))); | |||
}); | |||
return block; | |||
} | |||
function renderRecipes(reagentId) { | |||
var reactions = state.productReactions[reagentId] || []; | |||
if (!reactions.length) { | |||
return null; | |||
} | |||
var block = element('div', 'scmc-chem__card-section'); | |||
block.appendChild(element('h4', 'scmc-chem__card-subtitle', reactions.length > 1 ? 'Рецепты' : 'Рецепт')); | |||
reactions.forEach(function (reaction, index) { | |||
block.appendChild(renderReaction(reaction, index)); | |||
}); | |||
return block; | |||
} | |||
function renderDoseBadges(reagent) { | |||
var properties = reagent.properties || {}; | |||
var row = element('div', 'scmc-chem__dose-row'); | |||
if (properties.overdose !== undefined) { | |||
row.appendChild(element('span', 'scmc-chem__dose scmc-chem__dose--warning', 'Передозировка: ' + formatAmount(properties.overdose))); | |||
} | |||
if (properties.criticalOverdose !== undefined) { | |||
row.appendChild(element('span', 'scmc-chem__dose scmc-chem__dose--danger', 'Критическая: ' + formatAmount(properties.criticalOverdose))); | |||
} | |||
return row.childNodes.length ? row : null; | |||
} | |||
function renderCard(entry) { | |||
var reagent = reagentById(entry.id); | |||
var details = element('details', 'scmc-chem__card'); | |||
var summary = element('summary', 'scmc-chem__card-summary'); | |||
var color = safeColor(reagent && reagent.properties && reagent.properties.color); | |||
var colorBar = element('span', 'scmc-chem__color'); | |||
var heading = element('span', 'scmc-chem__card-heading'); | |||
var originLabel = entry.origin === 'upstream-reference' ? 'базовый' : entry.origin === 'stories' ? 'Stories' : 'RMC14'; | |||
details.style.setProperty('--scmc-chem-color', color); | |||
colorBar.style.backgroundColor = color; | |||
append( | |||
heading, | |||
element('span', 'scmc-chem__card-name', capitalizeFirst(entry.name || (reagent && reagent.name) || entry.id)), | |||
element('code', 'scmc-chem__card-id', entry.id) | |||
); | |||
append( | |||
summary, | |||
colorBar, | |||
heading, | |||
element('span', 'scmc-chem__origin scmc-chem__origin--' + (entry.origin || 'unknown'), originLabel), | |||
element('span', 'scmc-chem__chevron', '') | |||
); | |||
details.appendChild(summary); | |||
var body = element('div', 'scmc-chem__card-body'); | |||
if (!reagent) { | |||
body.appendChild(element('p', 'scmc-chem__empty', 'Данные реагента не найдены.')); | |||
details.appendChild(body); | |||
return details; | |||
} | |||
if (reagent.description) { | |||
body.appendChild(element('p', 'scmc-chem__description', capitalizeFirst(reagent.description))); | |||
} | |||
append(body, renderDoseBadges(reagent), renderRecipes(entry.id), renderMetabolisms(reagent), renderPlantMetabolism(reagent)); | |||
if (!body.childNodes.length) { | |||
body.appendChild(element('p', 'scmc-chem__empty', 'Для этого реагента нет дополнительных данных.')); | |||
} | |||
details.appendChild(body); | |||
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 renderSection(group) { | |||
var section = element('section', 'scmc-chem__section'); | |||
var title = capitalizeFirst(group.path[group.path.length - 1] || 'Без раздела'); | |||
var heading = element('div', 'scmc-chem__section-heading'); | |||
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) { | |||
var results = root.querySelector('.scmc-chem__results'); | |||
var entries = entriesForTab(state.activeTab).filter(function (entry) { | |||
return matchesQuery(entry, reagentById(entry.id)); | |||
}); | |||
results.textContent = ''; | |||
root.querySelector('.scmc-chem__result-count').textContent = entries.length + ' из ' + entriesForTab(state.activeTab).length; | |||
if (!entries.length) { | |||
var empty = element('div', 'scmc-chem__no-results'); | |||
append( | |||
empty, | |||
element('strong', '', 'Ничего не найдено.'), | |||
element('span', '', ' Попробуй другое название или ID реагента.') | |||
); | |||
results.appendChild(empty); | |||
return; | |||
} | |||
groupEntries(entries).forEach(function (group) { | |||
results.appendChild(renderSection(group)); | |||
}); | |||
} | |||
function renderTabs(root) { | |||
var tabs = root.querySelector('.scmc-chem__tabs'); | |||
tabs.textContent = ''; | |||
TAB_DEFINITIONS.forEach(function (tab) { | |||
var count = entriesForTab(tab.id).length; | |||
var button = element('button', 'scmc-chem__tab', tab.label); | |||
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); | |||
}); | |||
} | |||
function renderApp(root) { | |||
root.textContent = ''; | |||
root.classList.add('scmc-chem'); | |||
var header = element('header', 'scmc-chem__hero'); | |||
var titleBlock = element('div', 'scmc-chem__hero-copy'); | |||
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.placeholder = 'Название или ID реагента…'; | |||
input.autocomplete = 'off'; | |||
input.addEventListener('input', function () { | |||
state.query = normalizeSearch(input.value); | |||
renderResults(root); | |||
}); | |||
search.appendChild(input); | |||
controls.appendChild(search); | |||
controls.appendChild(element('span', 'scmc-chem__result-count', '')); | |||
root.appendChild(controls); | |||
var tabs = element('div', 'scmc-chem__tabs'); | |||
tabs.setAttribute('role', 'tablist'); | |||
root.appendChild(tabs); | |||
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); | |||
renderResults(root); | |||
} | |||
function renderLoading(root) { | |||
root.textContent = ''; | |||
var block = element('div', 'scmc-chem__status scmc-chem__status--loading'); | |||
append( | |||
block, | |||
element('span', 'scmc-chem__spinner', ''), | |||
element('span', '', 'Загружаю химический каталог…') | |||
); | |||
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) { | |||
return data && | |||
data.schemaVersion === 1 && | |||
data.reagents && | |||
data.dependencies && | |||
data.reactions && | |||
data.guides && | |||
data.counts; | |||
} | |||
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() { | |||
var root = document.getElementById(ROOT_ID); | |||
if (root) { | |||
load(root); | |||
} | |||
} | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', init); | |||
} else { | |||
init(); | |||
} | |||
}()); | |||
Версия от 06:44, 10 августа 2026
/*
* Интерактивный каталог химии SSMC.
* Страница: MC:Химия
* Данные: DeferW/ssmc-wiki-data
*/
(function () {
'use strict';
var ROOT_ID = 'scmc-chemistry-app';
var DATA_URL = 'https://raw.githubusercontent.com/DeferW/ssmc-wiki-data/main/data/chemistry-catalog.json';
var NUMBER = new Intl.NumberFormat('ru-RU', {
maximumFractionDigits: 3
});
var TAB_DEFINITIONS = [
{ id: 'ordnance', label: 'Боевая химия' },
{ id: 'medicine', label: 'Медицина' },
{ id: 'drinks', label: 'Напитки' },
{ id: 'unlisted', label: 'Прочее' }
];
var DAMAGE_LABELS = {
Airloss: 'урона от кислородного голодания',
Asphyxiation: 'урона от кислородного голодания',
Bloodloss: 'урона от кровопотери',
Blunt: 'тупого урона',
Brute: 'механического урона',
Burn: 'термического урона',
Caustic: 'кислотного урона',
Cellular: 'клеточного урона',
Cold: 'холодового урона',
Genetic: 'генетического урона',
Heat: 'ожогового урона',
Piercing: 'колотого урона',
Poison: 'токсического урона',
Radiation: 'радиационного урона',
Slash: 'режущего урона',
Toxin: 'токсического урона'
};
var STATUS_LABELS = {
Jitter: 'дрожь',
RMCUnconscious: 'потерю сознания',
StatusEffectSeeingRainbow: 'галлюцинации',
Unconscious: 'потерю сознания'
};
var EMOTE_LABELS = {
Gasp: 'судорожный вдох',
Laugh: 'смех',
Scream: 'крик',
Yawn: 'зевоту'
};
var PLANT_ATTRIBUTE_LABELS = {
PlantAdjustGrowth: 'возраст растения',
PlantAdjustHealth: 'здоровье растения',
PlantAdjustMutationLevel: 'уровень мутации',
PlantAdjustMutationMod: 'модификатор мутации',
PlantAdjustNutrition: 'уровень питания',
PlantAdjustPests: 'уровень вредителей',
PlantAdjustPotency: 'потенцию растения',
PlantAdjustToxins: 'уровень токсинов',
PlantAdjustWater: 'уровень воды',
PlantAdjustWeeds: 'уровень сорняков',
PlantAffectGrowth: 'возраст растения'
};
var METABOLISM_LABELS = {
Alcohol: 'Алкоголь',
Drink: 'Напиток',
Food: 'Пища',
Medicine: 'Лекарство',
Narcotic: 'Наркотик',
Poison: 'Яд'
};
var state = {
data: null,
activeTab: 'ordnance',
query: '',
productReactions: Object.create(null)
};
function element(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 append(parent) {
var children = Array.prototype.slice.call(arguments, 1);
children.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 capitalizeFirst(value) {
var text = String(value || '');
if (!text) {
return text;
}
return text.charAt(0).toLocaleUpperCase('ru-RU') + text.slice(1);
}
function finishSentence(value) {
var text = capitalizeFirst(String(value || '').trim());
if (!text || /[.!?]$/.test(text)) {
return text;
}
return text + '.';
}
function safeColor(value) {
if (typeof value !== 'string') {
return '#7f8c8d';
}
if (/^#[0-9a-f]{3,8}$/i.test(value) || /^[a-z]+$/i.test(value)) {
return value;
}
return '#7f8c8d';
}
function tagName(tag) {
return String(tag || '').replace(/^!type:/, '');
}
function reagentById(id) {
return state.data.reagents[id] || state.data.dependencies[id] || null;
}
function reagentName(id) {
var reagent = reagentById(id);
return reagent && reagent.name ? reagent.name : id;
}
function normalizeSearch(value) {
return String(value || '').trim().toLocaleLowerCase('ru-RU');
}
function matchesQuery(entry, reagent) {
if (!state.query) {
return true;
}
return normalizeSearch([
entry.id,
entry.name,
reagent && reagent.name,
reagent && reagent.description,
entry.sectionPath && entry.sectionPath.join(' ')
].filter(Boolean).join(' ')).indexOf(state.query) !== -1;
}
function buildProductReactionIndex() {
state.productReactions = Object.create(null);
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 entriesForTab(tabId) {
if (tabId === 'unlisted') {
return (state.data.unlistedReagents || []).map(function (id) {
var reagent = state.data.reagents[id];
return {
id: id,
name: reagent ? reagent.name : id,
origin: reagent ? reagent.origin : 'unknown',
sectionPath: ['Вне игровых руководств'],
includedBy: 'catalog'
};
});
}
return state.data.guides[tabId] || [];
}
function effectValue(effect) {
var value = effect && effect.value;
return value && typeof value === 'object' ? value : {};
}
function lowerFirst(value) {
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) {
return finishSentence(text);
}
return finishSentence('С шансом ' + formatNumber(probability * 100) + '% ' + lowerFirst(text));
}
function conditionText(condition) {
var type = tagName(condition && condition.yamlTag);
var value = effectValue(condition);
if (type === 'ReagentThreshold') {
if (value.min !== undefined && value.max !== undefined) {
return 'при количестве от ' + formatAmount(value.min) + ' до ' + formatAmount(value.max);
}
if (value.min !== undefined) {
return 'при количестве от ' + formatAmount(value.min) + ' в организме';
}
if (value.max !== undefined) {
return 'при количестве до ' + formatAmount(value.max) + ' в организме';
}
}
if (type === 'Temperature') {
if (value.min !== undefined && value.max !== undefined) {
return 'при температуре от ' + formatNumber(value.min) + ' до ' + formatNumber(value.max) + ' K';
}
if (value.min !== undefined) {
return 'при температуре не ниже ' + formatNumber(value.min) + ' K';
}
if (value.max !== undefined) {
return 'при температуре не выше ' + formatNumber(value.max) + ' K';
}
}
if (type === 'MobStateCondition') {
return value.mobstate === 'Critical' ? 'в критическом состоянии' : 'в состоянии «' + (value.mobstate || 'неизвестно') + '»';
}
if (type === 'OrganType') {
return (value.shouldHave === false ? 'кроме организмов типа ' : 'для организмов типа ') + (value.type || 'неизвестно');
}
return type ? 'при выполнении условия «' + type + '»' : '';
}
function thresholdMinimum(effect) {
var conditions = effectValue(effect).conditions;
var minimum = null;
if (!Array.isArray(conditions)) {
return minimum;
}
conditions.forEach(function (condition) {
if (tagName(condition && condition.yamlTag) !== 'ReagentThreshold') {
return;
}
var value = effectValue(condition);
if (typeof value.min === 'number' && (minimum === null || value.min > minimum)) {
minimum = value.min;
}
});
return minimum;
}
function effectTier(effect, reagent) {
var minimum = thresholdMinimum(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 remainingConditions(effect, reagent) {
var conditions = effectValue(effect).conditions;
if (!Array.isArray(conditions)) {
return [];
}
return conditions.map(function (condition) {
if (tagName(condition && condition.yamlTag) === 'ReagentThreshold' && effectTier(effect, reagent) !== 'normal') {
return '';
}
return conditionText(condition);
}).filter(Boolean);
}
function damageChanges(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 action = entry.amount < 0 ? 'Лечит' : 'Наносит';
return action + ' ' + formatNumber(Math.abs(entry.amount)) + ' ед. ' + (DAMAGE_LABELS[entry.id] || ('урона типа «' + entry.id + '»')) + ' в секунду';
});
}
function doseDescription(tier, text) {
return { tier: tier, text: finishSentence(text) };
}
function addDoseEffects(result, reagent, overdoseText, criticalText) {
var properties = reagent.properties || {};
if (properties.overdose !== undefined && overdoseText) {
result.push(doseDescription('overdose', overdoseText));
}
if (properties.criticalOverdose !== undefined && criticalText) {
result.push(doseDescription('critical', criticalText));
}
}
function rmcEffectDescriptions(type, value, reagent) {
var result = [];
var potency = Number(value.potency || 0);
var actual = potency * 0.5;
var perSecond = potency * 0.25;
var healing;
if (type === 'Neogenetic') {
healing = perSecond * (actual > 2 ? 1.5 : 1);
result.push(doseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. механического урона в секунду'));
addDoseEffects(result, reagent,
'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду',
'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. ожогового и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
return result;
}
if (type === 'Anticorrosive') {
healing = perSecond * (actual > 2 ? 1.5 : 1);
result.push(doseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. термического урона в секунду'));
addDoseEffects(result, reagent,
'Наносит по ' + formatNumber(perSecond) + ' ед. тупого и токсического урона в секунду',
'Дополнительно наносит по ' + formatNumber(perSecond * 5) + ' ед. тупого и токсического урона в секунду');
return result;
}
if (type === 'Biocidic') {
result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. механического урона в секунду'));
addDoseEffects(result, reagent,
'Наносит ' + formatNumber(perSecond * 2) + ' ед. механического урона в секунду',
'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. механического урона в секунду');
return result;
}
if (type === 'Antitoxic') {
result.push(doseDescription('normal', 'Лечит по ' + formatNumber(perSecond * 2) + ' ед. токсического и генетического урона в секунду; удаляет 0,125 ед. токсичных веществ из крови в секунду'));
addDoseEffects(result, reagent, '', 'Дополнительно даёт 5% шанс потерять сознание на 10 секунд');
return result;
}
if (type === 'Oxygenating') {
result.push(doseDescription('normal', actual >= 3
? 'Полностью лечит урон от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду'
: 'Лечит ' + formatNumber(perSecond) + ' ед. урона от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду'));
addDoseEffects(result, reagent,
'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. токсического урона в секунду',
'Дополнительно наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
return result;
}
if (type === 'Electrogenetic') {
result.push(doseDescription('normal', 'При дефибрилляции лечит по ' + formatNumber(potency * 10) + ' ед. механического, термического и токсического урона; расходует 1 ед. препарата'));
return result;
}
if (type === 'Antihallucinogenic') {
result.push(doseDescription('normal', 'Удаляет по 2,5 ед. Токсина «Разрушитель разума» и космических наркотиков из крови; сокращает галлюцинации на ' + formatNumber(perSecond * 10) + ' сек. каждую секунду'));
addDoseEffects(result, reagent,
'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
'Дополнительно наносит по ' + formatNumber(perSecond) + ' ед. тупого и ожогового урона, а также ' + formatNumber(perSecond * 3) + ' ед. токсического урона в секунду');
return result;
}
if (type === 'Focusing') {
result.push(doseDescription('normal', 'Удаляет ' + formatAmount(perSecond) + ' алкоголя из крови и сокращает опьянение, заикание и дрожь на ' + formatNumber(perSecond * 2) + ' сек. каждую секунду' + (actual >= 3 ? '; также снимает немоту и слепоту' : '')));
addDoseEffects(result, reagent,
'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
'Дополнительно наносит ' + formatNumber(perSecond * 3) + ' ед. токсического урона в секунду');
return result;
}
if (type === 'Thermostabilizing') {
result.push(doseDescription('normal', 'Возвращает температуру тела к норме, изменяя её примерно на ' + formatNumber(perSecond * 60) + ' K в секунду'));
addDoseEffects(result, reagent,
'Вызывает потерю сознания на 40 секунд',
'Дополнительно даёт 5% шанс потерять сознание на 10 секунд');
return result;
}
if (type === 'Hemogenic') {
result.push(doseDescription('normal', 'Если персонаж не голоден, восстанавливает ' + formatNumber(perSecond) + ' сл крови и расходует ' + formatNumber(perSecond) + ' ед. питания в секунду'));
if (actual > 3) {
result.push(doseDescription('normal', 'При избытке крови наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду, а также замедляет'));
}
addDoseEffects(result, reagent,
'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду',
'Дополнительно расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду');
return result;
}
if (type === 'Ketogenic') {
result.push(doseDescription('normal', 'Расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду и ускоряет выведение алкоголя'));
addDoseEffects(result, reagent,
'Дополнительно расходует ' + formatNumber(perSecond * 5) + ' ед. питания, наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду и даёт ' + formatNumber(actual * 2.5) + '% шанс рвоты',
'Вызывает потерю сознания на 40 секунд');
return result;
}
if (type === 'Nutritious') {
var nutrition = ((Number(value.nutFactor || 0) * Number(value.nutMetabolism || 0)) + potency) * actual;
result.push(doseDescription('normal', 'Восстанавливает ' + formatNumber(nutrition) + ' ед. питания за цикл метаболизма'));
return result;
}
if (type === 'Toxic') {
result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду'));
addDoseEffects(result, reagent,
'Наносит ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду',
'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. токсического урона в секунду');
return result;
}
if (type === 'Corrosive') {
result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. кислотного урона в секунду'));
addDoseEffects(result, reagent,
'Наносит ' + formatNumber(perSecond * 2) + ' ед. кислотного урона в секунду',
'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. кислотного урона в секунду');
return result;
}
if (type === 'Hypoxemic') {
result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду и даёт 10% шанс судорожного вдоха'));
addDoseEffects(result, reagent,
'Наносит по ' + formatNumber(perSecond) + ' ед. тупого и токсического урона, а также ' + formatNumber(perSecond * 5) + ' ед. урона от кислородного голодания в секунду',
'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. токсического урона в секунду');
return result;
}
if (type === 'Carcinogenic') {
result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. клеточного урона в секунду'));
addDoseEffects(result, reagent,
'Наносит ' + formatNumber(perSecond * 2) + ' ед. клеточного урона в секунду',
'Дополнительно наносит ' + formatNumber(perSecond * 2) + ' ед. тупого урона в секунду');
return result;
}
if (type === 'Oxidizing') {
result.push(doseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду; в огненной смеси повышает интенсивность, но уменьшает длительность и радиус'));
addDoseEffects(result, reagent,
'Наносит ' + formatNumber(perSecond * 2) + ' ед. ожогового урона в секунду',
'Дополнительно наносит ' + formatNumber(perSecond * 5) + ' ед. ожогового урона в секунду');
return result;
}
if (type === 'Fueling') {
result.push(doseDescription('normal', 'Повышает воспламеняемость тела на ' + formatNumber(perSecond) + ' ед. в секунду; в огненной смеси увеличивает длительность и радиус, но снижает интенсивность'));
addDoseEffects(result, reagent,
'Повышает воспламеняемость тела на ' + formatNumber(perSecond * 2) + ' ед. в секунду',
'Поджигает тело с высокой интенсивностью');
return result;
}
if (type === 'Flowing') {
result.push(doseDescription('normal', 'Делает огненную смесь более текучей: увеличивает радиус, но снижает интенсивность и длительность горения'));
return result;
}
if (type === 'Viscous') {
result.push(doseDescription('normal', 'Замедляет движение на ' + formatNumber(perSecond) + ' сек. за каждый цикл и уменьшает радиус огненной смеси'));
addDoseEffects(result, reagent,
'Сильно замедляет движение на ' + formatNumber(perSecond * 2) + ' сек. за каждый цикл',
'Практически останавливает движение на ' + formatNumber(perSecond * 3) + ' сек. за каждый цикл');
return result;
}
return null;
}
function genericEffectDescription(effect, reagent) {
var type = tagName(effect && effect.yamlTag);
var value = effectValue(effect);
var probability = value.probability;
var tier = effectTier(effect, reagent);
var conditions = remainingConditions(effect, reagent);
var text = '';
if (probability === 0) {
return [];
}
var rmc = rmcEffectDescriptions(type, value, reagent);
if (rmc) {
return rmc;
}
if (type === 'HealthChange' || type === 'EqualHealthChange') {
text = damageChanges(value.damage, type === 'EqualHealthChange').join('; ');
} else if (type === 'AdjustReagent' && value.reagent) {
text = (value.amount >= 0 ? 'Добавляет ' : 'Удаляет ') + formatAmount(Math.abs(value.amount || 0)) + ' реагента «' + capitalizeFirst(reagentName(value.reagent)) + '» за цикл метаболизма';
} else if (type === 'MovespeedModifier') {
var walk = Math.round((Number(value.walkSpeedModifier || 1) - 1) * 100);
var sprint = Math.round((Number(value.sprintSpeedModifier || 1) - 1) * 100);
text = 'Изменяет скорость ходьбы на ' + (walk >= 0 ? '+' : '') + walk + '% и бега на ' + (sprint >= 0 ? '+' : '') + sprint + '%';
} else if (type === 'GenericStatusEffect') {
var status = STATUS_LABELS[value.key] || value.key || 'неизвестный статус';
text = value.type === 'Remove'
? 'Сокращает ' + status + ' на ' + formatNumber(value.time || 2) + ' сек.'
: 'Вызывает ' + status + ' на ' + formatNumber(value.time || 2) + ' сек.';
} else if (type === 'ModifyStatusEffect') {
var modifiedStatus = STATUS_LABELS[value.effectProto] || value.effectProto || 'неизвестный статус';
text = value.type === 'Remove'
? 'Сокращает ' + modifiedStatus + ' на ' + formatNumber(value.time || 2) + ' сек.'
: 'Вызывает ' + modifiedStatus + ' на ' + formatNumber(value.time || 2) + ' сек.';
} else if (type === 'Jitter') {
text = 'Вызывает дрожь на ' + formatNumber(value.time || 2) + ' сек.';
} else if (type === 'Emote') {
text = 'Вызывает ' + (EMOTE_LABELS[value.emote] || ('эмоцию «' + (value.emote || 'неизвестно') + '»'));
} else if (type === 'ChemVomit') {
text = 'Вызывает рвоту';
} else if (type === 'Drunk') {
text = 'Добавляет ' + formatNumber(value.boozePower || 3) + ' сек. опьянения за цикл метаболизма';
} else if (type === 'SatiateHunger') {
text = 'Восстанавливает ' + formatNumber(value.factor !== undefined ? value.factor : 3) + ' ед. насыщения за цикл метаболизма';
} else if (type === 'SatiateThirst') {
text = 'Восстанавливает ' + formatNumber(value.factor !== undefined ? value.factor : 3) + ' ед. жажды за цикл метаболизма';
} else if (type === 'ChemHealEyeDamage') {
text = 'Лечит повреждения глаз';
} else if (type === 'CauseZombieInfection') {
text = 'Заражает зомби-инфекцией';
} else if (type === 'CureZombieInfection') {
text = value.innoculate ? 'Лечит зомби-инфекцию и создаёт иммунитет' : 'Лечит зомби-инфекцию';
} else if (type === 'FlammableReaction') {
text = 'Покрывает цель горючим; интенсивность составляет ' + formatNumber(Number(value.multiplier || 1) * 100) + '% от стандартной';
} else if (type === 'PopupMessage') {
text = value.messages && value.messages.indexOf('rmc-body-stings') !== -1
? 'Вызывает болезненное жжение'
: 'Вызывает заметное субъективное ощущение действия препарата';
} else {
text = 'Дополнительный игровой эффект «' + (type || 'неизвестно') + '»';
}
if (conditions.length) {
text += ' (' + conditions.join(', ') + ')';
}
return [doseDescription(tier, withChance(text, probability))];
}
function renderEffectText(text) {
var item = element('li', 'scmc-chem__effect');
item.appendChild(element('span', 'scmc-chem__effect-name', finishSentence(text)));
return item;
}
function tierTitle(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 appendEffectTier(group, tier, effects, reagent) {
if (!effects.length) {
return;
}
var tierBlock = element('div', 'scmc-chem__effect-tier scmc-chem__effect-tier--' + tier);
tierBlock.appendChild(element('div', 'scmc-chem__effect-tier-title', tierTitle(tier, reagent)));
var list = element('ul', 'scmc-chem__effect-list');
effects.forEach(function (description) {
list.appendChild(renderEffectText(description.text));
});
tierBlock.appendChild(list);
group.appendChild(tierBlock);
}
function renderMetabolisms(reagent) {
var metabolisms = reagent.properties && reagent.properties.metabolisms;
if (!metabolisms || !Object.keys(metabolisms).length) {
return null;
}
var block = element('div', 'scmc-chem__card-section');
var hasContent = false;
block.appendChild(element('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) {
genericEffectDescription(effect, reagent).forEach(function (description) {
tiers[description.tier].push(description);
});
});
if (!tiers.normal.length && !tiers.overdose.length && !tiers.critical.length) {
return;
}
var group = element('div', 'scmc-chem__metabolism');
var title = METABOLISM_LABELS[metabolismId] || metabolismId;
if (metabolism.metabolismRate !== undefined) {
title += ' · расход ' + formatAmount(metabolism.metabolismRate) + '/сек.';
}
group.appendChild(element('div', 'scmc-chem__metabolism-title', title));
appendEffectTier(group, 'normal', tiers.normal, reagent);
appendEffectTier(group, 'overdose', tiers.overdose, reagent);
appendEffectTier(group, 'critical', tiers.critical, reagent);
block.appendChild(group);
hasContent = true;
});
return hasContent ? block : null;
}
function plantEffectText(effect) {
var type = tagName(effect && effect.yamlTag);
var value = effectValue(effect);
var probability = value.probability;
var amount = Number(value.amount !== undefined ? value.amount : 1);
var attribute = PLANT_ATTRIBUTE_LABELS[type];
var text;
if (probability === 0) {
return '';
}
if (attribute) {
if (type === 'PlantAdjustHealth') {
text = amount >= 0
? 'Восстанавливает здоровье растения на ' + formatNumber(amount)
: 'Снижает здоровье растения на ' + formatNumber(Math.abs(amount));
} else {
text = (amount >= 0 ? 'Повышает ' : 'Снижает ') + attribute + ' на ' + formatNumber(Math.abs(amount));
}
} else if (type === 'PlantCryoxadone') {
text = 'Омолаживает растение с учётом его возраста и времени роста';
} else if (type === 'PlantPhalanximine') {
text = 'Возвращает жизнеспособность растению, погибшему из-за мутации';
} else if (type === 'PlantDiethylamine') {
text = 'Повышает продолжительность жизни или базовое здоровье растения; шанс 10% на единицу реагента';
} else if (type === 'PlantRestoreSeeds') {
text = 'Восстанавливает семена растения';
} else if (type === 'PlantDestroySeeds') {
text = 'Уничтожает семена растения';
} else {
text = 'Дополнительный растительный эффект «' + (type || 'неизвестно') + '»';
}
return withChance(text, probability);
}
function renderPlantMetabolism(reagent) {
var effects = reagent.properties && reagent.properties.plantMetabolism;
if (!Array.isArray(effects) || !effects.length) {
return null;
}
var descriptions = effects.map(plantEffectText).filter(Boolean);
if (!descriptions.length) {
return null;
}
var block = element('div', 'scmc-chem__card-section');
block.appendChild(element('h4', 'scmc-chem__card-subtitle', 'Метаболизм растений'));
var group = element('div', 'scmc-chem__metabolism');
group.appendChild(element('div', 'scmc-chem__metabolism-title', '1 ед. реагента каждые 3 секунды (базово)'));
var list = element('ul', 'scmc-chem__effect-list');
descriptions.forEach(function (description) {
list.appendChild(renderEffectText(description));
});
group.appendChild(list);
block.appendChild(group);
return block;
}
function renderMixture(items, className) {
var list = element('div', className);
(items || []).forEach(function (item) {
var row = element('div', 'scmc-chem__mixture-item');
var dot = element('span', 'scmc-chem__mixture-dot');
var reagent = reagentById(item.id);
dot.style.backgroundColor = safeColor(reagent && reagent.properties && reagent.properties.color);
append(
row,
dot,
element('span', 'scmc-chem__mixture-name', capitalizeFirst(item.name || reagentName(item.id))),
element('span', 'scmc-chem__mixture-amount', formatAmount(item.amount))
);
list.appendChild(row);
});
return list;
}
function reactionEffectText(effect) {
var type = tagName(effect && effect.yamlTag);
var value = effect && effect.value;
value = value && typeof value === 'object' ? value : {};
if (type === 'SensitiveReactionExplosionEffect') {
return 'Взрыв при объёме от ' + formatAmount(value.threshold || 0) + '; интенсивность за единицу: ' + formatNumber(value.intensityPerUnit || 0) + '.';
}
if (type === 'ExplosionReactionEffect') {
return 'Вызывает взрыв; интенсивность за единицу: ' + formatNumber(value.intensityPerUnit || 0) + '.';
}
if (type === 'AreaReactionEffect') {
return 'Создаёт эффект в области' + (value.duration ? ' на ' + formatNumber(value.duration) + ' сек.' : '.');
}
if (type === 'CreateEntityReactionEffect') {
return 'Создаёт объект: ' + (value.entity || 'неизвестно') + '.';
}
return type || 'Дополнительный эффект реакции';
}
function renderReaction(reaction, index) {
var block = element('div', 'scmc-chem__reaction');
if (index > 0) {
block.appendChild(element('div', 'scmc-chem__reaction-label', 'Вариант ' + (index + 1)));
}
var equation = element('div', 'scmc-chem__equation');
append(
equation,
renderMixture(reaction.reactants, 'scmc-chem__reactants'),
element('div', 'scmc-chem__arrow', '→'),
renderMixture(reaction.products, 'scmc-chem__products')
);
block.appendChild(equation);
if (reaction.conditions && reaction.conditions.minTemp !== undefined) {
block.appendChild(element('div', 'scmc-chem__reaction-note', 'Минимальная температура: ' + formatNumber(reaction.conditions.minTemp) + ' K.'));
}
(reaction.effects || []).forEach(function (effect) {
block.appendChild(element('div', 'scmc-chem__reaction-warning', reactionEffectText(effect)));
});
return block;
}
function renderRecipes(reagentId) {
var reactions = state.productReactions[reagentId] || [];
if (!reactions.length) {
return null;
}
var block = element('div', 'scmc-chem__card-section');
block.appendChild(element('h4', 'scmc-chem__card-subtitle', reactions.length > 1 ? 'Рецепты' : 'Рецепт'));
reactions.forEach(function (reaction, index) {
block.appendChild(renderReaction(reaction, index));
});
return block;
}
function renderDoseBadges(reagent) {
var properties = reagent.properties || {};
var row = element('div', 'scmc-chem__dose-row');
if (properties.overdose !== undefined) {
row.appendChild(element('span', 'scmc-chem__dose scmc-chem__dose--warning', 'Передозировка: ' + formatAmount(properties.overdose)));
}
if (properties.criticalOverdose !== undefined) {
row.appendChild(element('span', 'scmc-chem__dose scmc-chem__dose--danger', 'Критическая: ' + formatAmount(properties.criticalOverdose)));
}
return row.childNodes.length ? row : null;
}
function renderCard(entry) {
var reagent = reagentById(entry.id);
var details = element('details', 'scmc-chem__card');
var summary = element('summary', 'scmc-chem__card-summary');
var color = safeColor(reagent && reagent.properties && reagent.properties.color);
var colorBar = element('span', 'scmc-chem__color');
var heading = element('span', 'scmc-chem__card-heading');
var originLabel = entry.origin === 'upstream-reference' ? 'базовый' : entry.origin === 'stories' ? 'Stories' : 'RMC14';
details.style.setProperty('--scmc-chem-color', color);
colorBar.style.backgroundColor = color;
append(
heading,
element('span', 'scmc-chem__card-name', capitalizeFirst(entry.name || (reagent && reagent.name) || entry.id)),
element('code', 'scmc-chem__card-id', entry.id)
);
append(
summary,
colorBar,
heading,
element('span', 'scmc-chem__origin scmc-chem__origin--' + (entry.origin || 'unknown'), originLabel),
element('span', 'scmc-chem__chevron', '')
);
details.appendChild(summary);
var body = element('div', 'scmc-chem__card-body');
if (!reagent) {
body.appendChild(element('p', 'scmc-chem__empty', 'Данные реагента не найдены.'));
details.appendChild(body);
return details;
}
if (reagent.description) {
body.appendChild(element('p', 'scmc-chem__description', capitalizeFirst(reagent.description)));
}
append(body, renderDoseBadges(reagent), renderRecipes(entry.id), renderMetabolisms(reagent), renderPlantMetabolism(reagent));
if (!body.childNodes.length) {
body.appendChild(element('p', 'scmc-chem__empty', 'Для этого реагента нет дополнительных данных.'));
}
details.appendChild(body);
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 renderSection(group) {
var section = element('section', 'scmc-chem__section');
var title = capitalizeFirst(group.path[group.path.length - 1] || 'Без раздела');
var heading = element('div', 'scmc-chem__section-heading');
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) {
var results = root.querySelector('.scmc-chem__results');
var entries = entriesForTab(state.activeTab).filter(function (entry) {
return matchesQuery(entry, reagentById(entry.id));
});
results.textContent = '';
root.querySelector('.scmc-chem__result-count').textContent = entries.length + ' из ' + entriesForTab(state.activeTab).length;
if (!entries.length) {
var empty = element('div', 'scmc-chem__no-results');
append(
empty,
element('strong', '', 'Ничего не найдено.'),
element('span', '', ' Попробуй другое название или ID реагента.')
);
results.appendChild(empty);
return;
}
groupEntries(entries).forEach(function (group) {
results.appendChild(renderSection(group));
});
}
function renderTabs(root) {
var tabs = root.querySelector('.scmc-chem__tabs');
tabs.textContent = '';
TAB_DEFINITIONS.forEach(function (tab) {
var count = entriesForTab(tab.id).length;
var button = element('button', 'scmc-chem__tab', tab.label);
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);
});
}
function renderApp(root) {
root.textContent = '';
root.classList.add('scmc-chem');
var header = element('header', 'scmc-chem__hero');
var titleBlock = element('div', 'scmc-chem__hero-copy');
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.placeholder = 'Название или ID реагента…';
input.autocomplete = 'off';
input.addEventListener('input', function () {
state.query = normalizeSearch(input.value);
renderResults(root);
});
search.appendChild(input);
controls.appendChild(search);
controls.appendChild(element('span', 'scmc-chem__result-count', ''));
root.appendChild(controls);
var tabs = element('div', 'scmc-chem__tabs');
tabs.setAttribute('role', 'tablist');
root.appendChild(tabs);
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);
renderResults(root);
}
function renderLoading(root) {
root.textContent = '';
var block = element('div', 'scmc-chem__status scmc-chem__status--loading');
append(
block,
element('span', 'scmc-chem__spinner', ''),
element('span', '', 'Загружаю химический каталог…')
);
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) {
return data &&
data.schemaVersion === 1 &&
data.reagents &&
data.dependencies &&
data.reactions &&
data.guides &&
data.counts;
}
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() {
var root = document.getElementById(ROOT_ID);
if (root) {
load(root);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
}());