MediaWiki:ScmcChemistry.js: различия между версиями
Страница интерфейса MediaWiki
Дополнительные действия
Defer (обсуждение | вклад) Нет описания правки |
Defer (обсуждение | вклад) Нет описания правки |
||
| (не показаны 4 промежуточные версии этой же участницы) | |||
| Строка 2: | Строка 2: | ||
* Интерактивный каталог химии SSMC. | * Интерактивный каталог химии SSMC. | ||
* Страница: MC:Химия | * Страница: MC:Химия | ||
* Данные: | * Данные: MediaWiki:ScmcChemCatalog.js | ||
*/ | */ | ||
(function () { | (function (mw, $) { | ||
'use strict'; | 'use strict'; | ||
var ROOT_ID = 'scmc-chemistry-app'; | var ROOT_ID = 'scmc-chemistry-app'; | ||
var | var CATALOG_PAGE = 'MediaWiki:ScmcChemCatalog.js'; | ||
var NUMBER = new Intl.NumberFormat('ru-RU', { | var STYLE_PAGE = 'MediaWiki:ScmcChemistry.css'; | ||
var NUMBER = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 3 }); | |||
var TABS = [ | |||
var | |||
{ id: 'ordnance', label: 'Боевая химия' }, | { id: 'ordnance', label: 'Боевая химия' }, | ||
{ id: 'medicine', label: 'Медицина' }, | { id: 'medicine', label: 'Медицина' }, | ||
| Строка 20: | Строка 18: | ||
{ id: 'other', label: 'Другие вещества' } | { id: 'other', label: 'Другие вещества' } | ||
]; | ]; | ||
var ORIGIN_LABELS = { | |||
var | rmc14: 'RMC14', | ||
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', | 'наносит': 'harmful', 'дополнительно наносит': 'harmful', 'вызывает': 'harmful', | ||
'заражает': 'harmful', 'поджигает': 'harmful', 'уничтожает': 'harmful', | |||
'расходует': 'warning', 'замедляет': 'warning', | |||
'удаляет': 'beneficial', | 'снижает': 'utility', 'повышает': 'utility' | ||
'наносит': 'harmful', | |||
'заражает': 'harmful', | |||
'расходует': 'warning', | |||
'снижает': '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: 'потерю сознания', | ||
StatusEffectSeeingRainbow: 'галлюцинации', Unconscious: 'потерю сознания' | |||
StatusEffectSeeingRainbow: 'галлюцинации', | |||
}; | }; | ||
var EMOTE_LABELS = { | var EMOTE_LABELS = { | ||
Gasp: 'судорожный вдох', | Gasp: 'судорожный вдох', Laugh: 'смех', Scream: 'крик', Yawn: 'зевоту' | ||
}; | }; | ||
var PLANT_ATTRIBUTE_LABELS = { | var PLANT_ATTRIBUTE_LABELS = { | ||
PlantAdjustGrowth: 'возраст растения', | PlantAdjustGrowth: 'возраст растения', PlantAdjustHealth: 'здоровье растения', | ||
PlantAdjustMutationLevel: 'уровень мутации', PlantAdjustMutationMod: 'модификатор мутации', | |||
PlantAdjustMutationLevel: 'уровень мутации', | PlantAdjustNutrition: 'уровень питания', PlantAdjustPests: 'уровень вредителей', | ||
PlantAdjustPotency: 'потенцию растения', PlantAdjustToxins: 'уровень токсинов', | |||
PlantAdjustNutrition: 'уровень питания', | PlantAdjustWater: 'уровень воды', PlantAdjustWeeds: 'уровень сорняков', | ||
PlantAdjustPotency: 'потенцию растения', | |||
PlantAdjustWater: 'уровень воды', | |||
PlantAffectGrowth: 'возраст растения' | PlantAffectGrowth: 'возраст растения' | ||
}; | }; | ||
var REAGENT_LABELS = { | |||
var | UncookedAnimalProteins: 'сырые животные белки' | ||
}; | }; | ||
var state = { | var state = { | ||
data: null, | data: null, | ||
| Строка 128: | Строка 96: | ||
}; | }; | ||
function | function el(tag, className, text) { | ||
var node = document.createElement(tag); | var node = document.createElement(tag); | ||
if (className) { | if (className) { | ||
node.className = className; | node.className = className; | ||
} | } | ||
if (text !== undefined && text !== null) { | if (text !== undefined && text !== null) { | ||
node.textContent = String(text); | node.textContent = String(text); | ||
} | } | ||
return node; | return node; | ||
} | } | ||
function | function add(parent) { | ||
Array.prototype.slice.call(arguments, 1).forEach(function (child) { | |||
if (child) { | if (child) { | ||
parent.appendChild(child); | parent.appendChild(child); | ||
} | } | ||
}); | }); | ||
return parent; | return parent; | ||
} | } | ||
| Строка 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 | 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 | 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 | function renderTabs(root) { | ||
var | var tabs = root.querySelector('.scmc-chem__tabs'); | ||
tabs.textContent = ''; | |||
TABS.forEach(function (tab) { | |||
var button = el('button', 'scmc-chem__tab'); | |||
button.type = 'button'; | |||
button.setAttribute('role', 'tab'); | |||
button.setAttribute('aria-selected', String(tab.id === state.activeTab)); | |||
add(button, | |||
el('span', '', tab.label), | |||
el('span', 'scmc-chem__tab-count', state.data.catalogSections[tab.id].length) | |||
); | |||
button.addEventListener('click', function () { | |||
state.activeTab = tab.id; | |||
renderTabs(root); | |||
renderResults(root); | |||
}); | |||
add(tabs, button); | |||
}); | |||
} | |||
function 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 (! | function entryMatches(entry) { | ||
return | 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 | 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); | |||
} | |||
return ' | 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 | 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 | 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 | function effectKind(type, value) { | ||
var | var beneficial = /^(Anticorrosive|Antihallucinogenic|Antitoxic|Biocidic|ChemHealEyeDamage|CureZombieInfection|Electrogenetic|Focusing|Hemogenic|Neogenetic|Nutritious|Oxygenating|SatiateHunger|SatiateThirst|Thermostabilizing)$/; | ||
return | 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 | function conditionText(condition, reagent, hideDoseCondition) { | ||
return | 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 | function renderEffect(effect, reagent, tier) { | ||
if (! | 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 | 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 | function plantEffectKind(type, amount) { | ||
return | if (/Mutation/.test(type)) { | ||
return 'warning'; | |||
} | |||
var harmfulWhenPositive = /Toxins|Weeds|Pests/.test(type); | |||
return (harmfulWhenPositive ? amount <= 0 : amount >= 0) ? 'beneficial' : 'harmful'; | |||
} | } | ||
function | 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 | function originalWithChance(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 | function originalConditionText(condition) { | ||
var type = tagName(condition && condition.yamlTag); | var type = tagName(condition && condition.yamlTag); | ||
var value = | 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 | function originalThresholdMinimum(effect) { | ||
var 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 = | |||
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 | function originalEffectTier(effect, reagent) { | ||
var minimum = | 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 | function originalRemainingConditions(effect, reagent) { | ||
var 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' && | if (tagName(condition && condition.yamlTag) === 'ReagentThreshold' && originalEffectTier(effect, reagent) !== 'normal') { | ||
return ''; | return ''; | ||
} | } | ||
return | return originalConditionText(condition); | ||
}).filter(Boolean); | }).filter(Boolean); | ||
} | } | ||
function | 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 | var actionText = entry.amount < 0 ? 'Лечит' : 'Наносит'; | ||
return | return actionText + ' ' + formatNumber(Math.abs(entry.amount)) + ' ед. ' + | ||
(DAMAGE_LABELS[entry.id] || ('урона типа «' + entry.id + '»')) + ' в секунду'; | |||
}); | }); | ||
} | } | ||
function | function originalDoseDescription(tier, text) { | ||
return { tier: tier, text: finishSentence(text) }; | return { tier: tier, text: finishSentence(text) }; | ||
} | } | ||
function | 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( | result.push(originalDoseDescription('overdose', overdoseText)); | ||
} | } | ||
if (properties.criticalOverdose !== undefined && criticalText) { | if (properties.criticalOverdose !== undefined && criticalText) { | ||
result.push( | result.push(originalDoseDescription('critical', criticalText)); | ||
} | } | ||
} | } | ||
function | function originalRmcEffectDescriptions(type, value, reagent) { | ||
var result = []; | var result = []; | ||
var potency = Number(value.potency || 0); | var potency = Number(value.potency || 0); | ||
var actual = potency | 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( | result.push(originalDoseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. механического урона в секунду')); | ||
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( | result.push(originalDoseDescription('normal', 'Лечит ' + formatNumber(healing) + ' ед. термического урона в секунду')); | ||
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( | result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. механического урона в секунду')); | ||
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( | result.push(originalDoseDescription('normal', 'Лечит по ' + formatNumber(perSecond * 2) + ' ед. токсического и генетического урона в секунду; удаляет 0,125 ед. токсичных веществ из крови в секунду')); | ||
originalAddDoseEffects(result, reagent, '', 'Дополнительно даёт 5% шанс потерять сознание на 10 секунд'); | |||
return result; | return result; | ||
} | } | ||
if (type === 'Oxygenating') { | if (type === 'Oxygenating') { | ||
result.push( | result.push(originalDoseDescription('normal', actual >= 3 ? | ||
'Полностью лечит урон от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду' : | |||
'Лечит ' + formatNumber(perSecond) + ' ед. урона от кислородного голодания и удаляет ' + formatAmount(perSecond) + ' Лексорина из крови в секунду')); | |||
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( | result.push(originalDoseDescription('normal', 'При дефибрилляции лечит по ' + formatNumber(potency * 10) + ' ед. механического, термического и токсического урона; расходует 1 ед. препарата')); | ||
return result; | return result; | ||
} | } | ||
if (type === 'Antihallucinogenic') { | if (type === 'Antihallucinogenic') { | ||
result.push( | result.push(originalDoseDescription('normal', 'Удаляет по 2,5 ед. Токсина «Разрушитель разума» и космических наркотиков из крови; сокращает галлюцинации на ' + formatNumber(perSecond * 10) + ' сек. каждую секунду')); | ||
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( | result.push(originalDoseDescription('normal', 'Удаляет ' + formatAmount(perSecond) + ' алкоголя из крови и сокращает опьянение, заикание и дрожь на ' + formatNumber(perSecond * 2) + ' сек. каждую секунду' + (actual >= 3 ? '; также снимает немоту и слепоту' : ''))); | ||
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( | result.push(originalDoseDescription('normal', 'Возвращает температуру тела к норме, изменяя её примерно на ' + formatNumber(perSecond * 60) + ' K в секунду')); | ||
originalAddDoseEffects(result, reagent, 'Вызывает потерю сознания на 40 секунд', 'Дополнительно даёт 5% шанс потерять сознание на 10 секунд'); | |||
return result; | return result; | ||
} | } | ||
if (type === 'Hemogenic') { | if (type === 'Hemogenic') { | ||
result.push( | result.push(originalDoseDescription('normal', 'Если персонаж не голоден, восстанавливает ' + formatNumber(perSecond) + ' сл крови и расходует ' + formatNumber(perSecond) + ' ед. питания в секунду')); | ||
if (actual > 3) { | if (actual >= 3) { | ||
result.push( | result.push(originalDoseDescription('normal', 'При избытке крови наносит ' + formatNumber(perSecond) + ' ед. тупого и ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду, а также замедляет')); | ||
} | } | ||
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( | result.push(originalDoseDescription('normal', 'Расходует ' + formatNumber(perSecond * 5) + ' ед. питания в секунду и ускоряет выведение алкоголя')); | ||
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( | result.push(originalDoseDescription('normal', 'Восстанавливает ' + formatNumber(nutrition) + ' ед. питания за цикл метаболизма')); | ||
return result; | return result; | ||
} | } | ||
if (type === 'Toxic') { | if (type === 'Toxic') { | ||
result.push( | result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. токсического урона в секунду')); | ||
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( | result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. кислотного урона в секунду')); | ||
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( | result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond * 2) + ' ед. урона от кислородного голодания в секунду и даёт 10% шанс судорожного вдоха')); | ||
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( | result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond * 0.5) + ' ед. клеточного урона в секунду')); | ||
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( | result.push(originalDoseDescription('normal', 'Наносит ' + formatNumber(perSecond) + ' ед. ожогового урона в секунду; в огненной смеси повышает интенсивность, но уменьшает длительность и радиус')); | ||
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( | result.push(originalDoseDescription('normal', 'Повышает воспламеняемость тела на ' + formatNumber(perSecond) + ' ед. в секунду; в огненной смеси увеличивает длительность и радиус, но снижает интенсивность')); | ||
originalAddDoseEffects(result, reagent, | |||
'Повышает воспламеняемость тела на ' + formatNumber(perSecond * 2) + ' ед. в секунду', | 'Повышает воспламеняемость тела на ' + formatNumber(perSecond * 2) + ' ед. в секунду', | ||
'Поджигает тело с высокой интенсивностью'); | 'Поджигает тело с высокой интенсивностью'); | ||
return result; | return result; | ||
} | } | ||
if (type === 'Flowing') { | if (type === 'Flowing') { | ||
result.push( | result.push(originalDoseDescription('normal', 'Делает огненную смесь более текучей: увеличивает радиус, но снижает интенсивность и длительность горения')); | ||
return result; | return result; | ||
} | } | ||
if (type === 'Viscous') { | if (type === 'Viscous') { | ||
result.push( | result.push(originalDoseDescription('normal', 'Замедляет движение на ' + formatNumber(perSecond) + ' сек. за каждый цикл и уменьшает радиус огненной смеси')); | ||
originalAddDoseEffects(result, reagent, | |||
'Сильно замедляет движение на ' + formatNumber(perSecond * 2) + ' сек. за каждый цикл', | 'Сильно замедляет движение на ' + formatNumber(perSecond * 2) + ' сек. за каждый цикл', | ||
'Практически останавливает движение на ' + formatNumber(perSecond * 3) + ' сек. за каждый цикл'); | 'Практически останавливает движение на ' + formatNumber(perSecond * 3) + ' сек. за каждый цикл'); | ||
return result; | return result; | ||
} | } | ||
return null; | return null; | ||
} | } | ||
function | function originalGenericEffectDescription(effect, reagent) { | ||
var type = tagName(effect && effect.yamlTag); | var type = tagName(effect && effect.yamlTag); | ||
var value = | var value = originalEffectValue(effect); | ||
var probability = value.probability; | var probability = value.probability; | ||
var tier = | var tier = originalEffectTier(effect, reagent); | ||
var conditions = | 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 = | |||
if (rmc) { | if (rmc) { | ||
return rmc; | return rmc; | ||
} | } | ||
if (type === 'HealthChange' || type === 'EqualHealthChange') { | if (type === 'HealthChange' || type === 'EqualHealthChange') { | ||
text = | 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) + ' сек.'; | ||
} 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) + ' сек.'; | ||
} 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 [ | |||
} | } | ||
function | 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 | 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' }); } | ||
if (warning !== -1) { candidates.push({ index: warning, tone: 'warning' }); } | |||
candidates.sort(function (left, right) { return left.index - right.index; }); | |||
if (harmful !== -1) { | |||
if (warning !== -1) { | |||
candidates.sort(function (left, right) { | |||
return candidates.length ? candidates[0].tone : 'utility'; | return candidates.length ? candidates[0].tone : 'utility'; | ||
} | } | ||
function | 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 = | parent.appendChild(className ? el('span', className, match[0]) : document.createTextNode(match[0])); | ||
parent.appendChild(className | |||
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 | function originalRenderEffectText(text) { | ||
var tone = | var tone = originalEffectTone(text); | ||
var item = | var item = el('li', 'scmc-chem__effect scmc-chem__effect--' + tone); | ||
var content = | var content = el('span', 'scmc-chem__effect-name'); | ||
originalAppendSemanticEffectText(content, text); | |||
item.appendChild(content); | item.appendChild(content); | ||
return item; | return item; | ||
} | } | ||
function | 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 | 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 = | tierBlock.appendChild(el('div', 'scmc-chem__effect-tier-title', originalTierTitle(tier, reagent))); | ||
tierBlock.appendChild( | var list = el('ul', 'scmc-chem__effect-list'); | ||
var list = | effects.forEach(function (description) { list.appendChild(originalRenderEffectText(description.text)); }); | ||
effects.forEach(function (description) { | |||
tierBlock.appendChild(list); | tierBlock.appendChild(list); | ||
group.appendChild(tierBlock); | group.appendChild(tierBlock); | ||
} | } | ||
function | 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 = | |||
var hasContent = false; | var hasContent = false; | ||
block.appendChild( | 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) { | ||
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 = | |||
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( | originalAppendEffectTier(group, 'normal', tiers.normal, reagent); | ||
originalAppendEffectTier(group, 'overdose', tiers.overdose, reagent); | |||
originalAppendEffectTier(group, 'critical', tiers.critical, reagent); | |||
block.appendChild(group); | block.appendChild(group); | ||
hasContent = true; | hasContent = true; | ||
}); | }); | ||
return hasContent ? block : null; | return hasContent ? block : null; | ||
} | } | ||
function | function originalPlantEffectText(effect) { | ||
var type = tagName(effect && effect.yamlTag); | var type = tagName(effect && effect.yamlTag); | ||
var value = | 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)); | ||
} 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 | |||
} | } | ||
function | 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( | |||
if (!descriptions.length) { | if (!descriptions.length) { | ||
return null; | return null; | ||
} | } | ||
var block = el('div', 'scmc-chem__card-section'); | |||
var block = | block.appendChild(el('h4', 'scmc-chem__card-subtitle', 'Метаболизм растений')); | ||
block.appendChild( | var group = el('div', 'scmc-chem__metabolism'); | ||
var group = | group.appendChild(el('div', 'scmc-chem__metabolism-title', '1 ед. реагента каждые 3 секунды (базово)')); | ||
group.appendChild( | var list = el('ul', 'scmc-chem__effect-list'); | ||
var list = | descriptions.forEach(function (description) { list.appendChild(originalRenderEffectText(description)); }); | ||
descriptions.forEach(function (description) { | |||
group.appendChild(list); | group.appendChild(list); | ||
block.appendChild(group); | block.appendChild(group); | ||
return block; | return block; | ||
} | } | ||
function renderCard(entry) { | function renderCard(entry) { | ||
var reagent = | var reagent = getReagent(entry.id) || { | ||
id: entry.id, | |||
name: entry.name, | |||
var color = safeColor( | origin: entry.origin, | ||
var | properties: {} | ||
}; | |||
var color = safeColor(reagent.properties && reagent.properties.color); | |||
var details = el('details', 'scmc-chem__card'); | |||
details.style.setProperty('--scmc-chem-color', color); | details.style.setProperty('--scmc-chem-color', color); | ||
var summary = el('summary', 'scmc-chem__card-summary'); | |||
var heading = add(el('span', 'scmc-chem__card-heading'), | |||
el('span', 'scmc-chem__card-name', capitalizeFirst(reagent.name || entry.name || entry.id)), | |||
el('code', 'scmc-chem__card-id', entry.id) | |||
); | ); | ||
add(summary, | |||
el('span', 'scmc-chem__color'), | |||
heading, | heading, | ||
el('span', 'scmc-chem__origin scmc-chem__origin--' + (reagent.origin || entry.origin || 'upstream-reference'), ORIGIN_LABELS[reagent.origin || entry.origin] || reagent.origin || entry.origin), | |||
el('span', 'scmc-chem__chevron') | |||
); | ); | ||
summary.firstChild.style.backgroundColor = color; | |||
var body = el('div', 'scmc-chem__card-body'); | |||
var body = | add(body, el('p', reagent.description ? 'scmc-chem__description' : 'scmc-chem__empty', reagent.description || 'Описание не указано.')); | ||
var overdose = reagent.properties && reagent.properties.overdose; | |||
if (! | var critical = reagent.properties && reagent.properties.criticalOverdose; | ||
if (overdose !== undefined || critical !== undefined) { | |||
var doses = el('div', 'scmc-chem__dose-row'); | |||
if (overdose !== undefined) { | |||
add(doses, el('span', 'scmc-chem__dose scmc-chem__dose--warning', 'Передозировка: ' + formatAmount(overdose))); | |||
} | |||
if (critical !== undefined) { | |||
add(doses, el('span', 'scmc-chem__dose scmc-chem__dose--danger', 'Критическая: ' + formatAmount(critical))); | |||
} | |||
add(body, doses); | |||
} | } | ||
var reactions = state.productReactions[entry.id] || []; | |||
if ( | if (reactions.length) { | ||
var recipeSection = el('div', 'scmc-chem__card-section'); | |||
add(recipeSection, el('h4', 'scmc-chem__card-subtitle', 'Получение')); | |||
reactions.forEach(function (reaction) { add(recipeSection, renderReaction(reaction)); }); | |||
add(body, recipeSection); | |||
} | } | ||
add(body, renderOriginalMetabolisms(reagent)); | |||
add(body, renderOriginalPlantMetabolism(reagent)); | |||
add(details, summary, body); | |||
return details; | return details; | ||
} | } | ||
function renderResults(root) { | function renderResults(root) { | ||
var | var result = root.querySelector('.scmc-chem__results'); | ||
var | var counter = root.querySelector('.scmc-chem__result-count'); | ||
result.textContent = ''; | |||
var entries = state.data.catalogSections[state.activeTab].filter(entryMatches); | |||
var groups = groupEntries(entries); | |||
counter.textContent = entries.length + ' найдено'; | |||
renderSectionNav(root, groups); | |||
if (!entries.length) { | if (!entries.length) { | ||
add(result, el('div', 'scmc-chem__no-results', 'Ничего не найдено. Измените запрос.')); | |||
return; | return; | ||
} | } | ||
groups.forEach(function (group, index) { | groups.forEach(function (group, index) { | ||
var section = el('section', 'scmc-chem__section'); | |||
section.id = 'scmc-chem-section-' + index + '-' + slug(group.title); | |||
var heading = add(el('div', 'scmc-chem__section-heading'), | |||
el('h2', 'scmc-chem__section-title', 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)); }); | |||
var | add(section, heading, cards); | ||
add(result, section); | |||
}); | }); | ||
} | } | ||
| Строка 1109: | Строка 1233: | ||
function renderApp(root) { | function renderApp(root) { | ||
root.textContent = ''; | root.textContent = ''; | ||
add(root, createHero()); | |||
var controls = el('div', 'scmc-chem__controls'); | |||
var search = el('label', 'scmc-chem__search'); | |||
var input = el('input', 'scmc-chem__search-input'); | |||
var controls = | |||
var search = | |||
var input = | |||
input.type = 'search'; | input.type = 'search'; | ||
input.placeholder = 'Название или | input.placeholder = 'Название, ID или раздел…'; | ||
input.autocomplete = 'off'; | input.autocomplete = 'off'; | ||
input.setAttribute('aria-label', 'Поиск по химическому каталогу'); | |||
input.addEventListener('input', function () { | input.addEventListener('input', function () { | ||
state.query = | state.query = normalize(input.value); | ||
renderResults(root); | renderResults(root); | ||
}); | }); | ||
add(search, el('span', 'scmc-chem__search-icon', '⌕'), input); | |||
add(controls, search, el('span', 'scmc-chem__result-count')); | |||
add(root, controls, el('div', 'scmc-chem__tabs')); | |||
var nav = el('nav', 'scmc-chem__section-nav'); | |||
nav.setAttribute('aria-label', 'Подразделы каталога'); | |||
add(root, nav, el('main', 'scmc-chem__results')); | |||
var source = state.data.source || {}; | |||
add(root, el('footer', 'scmc-chem__footer', 'Локальный каталог MediaWiki' + (source.commit ? ' · версия ' + String(source.commit).slice(0, 8) : ''))); | |||
var | |||
var | |||
renderTabs(root); | renderTabs(root); | ||
renderResults(root); | renderResults(root); | ||
} | } | ||
function | function start(root) { | ||
root | renderLoading(root); | ||
loadCatalog().then(function (data) { | |||
state.data = data; | |||
indexReactions(); | |||
renderApp(root); | |||
}).catch(function (error) { | |||
renderError(root, error); | |||
}); | }); | ||
} | } | ||
function init() { | function init() { | ||
var root = document.getElementById(ROOT_ID); | var root = document.getElementById(ROOT_ID); | ||
if (!root) { | |||
if (root) { | return; | ||
} | } | ||
loadStyle(); | |||
start(root); | |||
} | } | ||
$(init); | |||
}(mediaWiki, jQuery)); | |||
}()); | |||
Текущая версия от 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));