Модуль:Reagents
Для документации этого модуля может быть создана страница Модуль:Reagents/doc
-- Module:Reagents
-- Renders reagent cards from game prototype JSON data.
--
-- Data source: Module:Reagents/reagent.json (511 reagents)
-- Module:Reagents/reaction.json (414 reactions)
--
-- Usage:
-- {{#invoke:Reagents|group|Medicine}} — all cards in a group
-- {{#invoke:Reagents|card|Bicaridine}} — single card by prototype id
-- {{#invoke:Reagents|reactions|Medicine}} — reactions producing reagents in group
-- {{#invoke:Reagents|count|Medicine}} — count of reagents in group
local reagents = mw.loadJsonData( 'Module:Reagents/reagent.json' )
local p = {}
-- ── helpers ─────────────────────────────────────────────────
local function attr( s )
return s:gsub( '"', '"' )
end
local function cssColor( raw )
if not raw or raw == '' then return nil end
if raw:sub(1,1) == '#' then return raw end
return raw
end
-- Translate damage type names to Russian
local dmgNames = {
Brute = 'Физ.',
Burn = 'Ожог',
Heat = 'Тепло',
Cold = 'Холод',
Shock = 'Шок',
Caustic = 'Кислот.',
Poison = 'Токсин',
Radiation = 'Рад.',
Asphyxiation = 'Удушье',
Bloodloss = 'Кровопот.',
Cellular = 'Клеточн.',
Piercing = 'Прокол',
Slash = 'Режущ.',
Blunt = 'Дробящ.',
Structural = 'Структ.',
}
local function trDmg( name )
return dmgNames[name] or name
end
-- Format a damage value: "−Физ. 2.5" or "+Токсин 1.0"
local function fmtDmg( name, val )
local sign = val < 0 and '−' or '+'
local abs = math.abs( val )
local num = abs == math.floor( abs ) and tostring( math.floor( abs ) ) or string.format( '%.1f', abs )
return sign .. trDmg( name ) .. ' ' .. num
end
-- Parse HealthChange effects from metabolism data.
local function parseEffects( metabs )
local heal = {}
local odHeal = {}
local odThreshold = nil
local rate = nil
local special = {}
if not metabs then return heal, odHeal, odThreshold, rate, special end
for _, metaData in pairs( metabs ) do
if metaData.rate then
rate = metaData.rate
end
for _, eff in ipairs( metaData.effects or {} ) do
local etype = eff.type or ''
if etype:find( 'HealthChange' ) then
local isOD = false
local threshold = nil
for _, cond in ipairs( eff.conditions or {} ) do
if (cond.type or ''):find( 'ReagentThreshold' ) then
threshold = cond.min
if threshold and threshold > 0 then
isOD = true
end
end
end
local dmg = eff.damage or {}
local target = isOD and odHeal or heal
if isOD and threshold then
odThreshold = odThreshold and math.min( odThreshold, threshold ) or threshold
end
for gName, gVal in pairs( dmg.groups or {} ) do
target[#target + 1] = fmtDmg( gName, gVal )
end
for tName, tVal in pairs( dmg.types or {} ) do
target[#target + 1] = fmtDmg( tName, tVal )
end
elseif etype:find( 'SuppressPain' ) then
special[#special + 1] = 'подавляет боль'
elseif etype:find( 'Jitter' ) then
special[#special + 1] = 'дрожь'
elseif etype:find( 'Drunk' ) then
special[#special + 1] = 'опьянение'
elseif etype:find( 'ChemVomit' ) then
special[#special + 1] = 'рвота'
elseif etype:find( 'AdjustTemperature' ) then
special[#special + 1] = 'изм. температуры'
elseif etype:find( 'Emote' ) then
special[#special + 1] = 'эмоция'
elseif etype:find( 'MovespeedModifier' ) then
special[#special + 1] = 'изм. скорости'
elseif etype:find( 'GenericStatusEffect' ) or etype:find( 'StatusEffect' ) then
local proto = eff.effectProto or eff.key or ''
if proto:find( 'Stun' ) then special[#special + 1] = 'стан'
elseif proto:find( 'Sleep' ) then special[#special + 1] = 'сон'
elseif proto:find( 'Mute' ) then special[#special + 1] = 'немота'
elseif proto:find( 'Blind' ) then special[#special + 1] = 'слепота'
elseif proto:find( 'Rainbow' ) then special[#special + 1] = 'галлюцинации'
end
elseif etype:find( 'AdjustReagent' ) then
-- skip: internal reagent conversion
end
end
end
return heal, odHeal, odThreshold, rate, special
end
-- Mixer/method translation
local mixerNames = {
Centrifuge = 'Центрифуга',
Electrolysis = 'Электролиз',
Shake = 'Встряхивание',
Stir = 'Размешивание',
Holy = 'Освящение',
}
-- Build recipe HTML from a reagent's recipes array
-- Returns recipeLine, methodTag
local function buildRecipe( r )
local recipes = r.recipes
if not recipes or not recipes[1] then return nil, nil end
local rec = recipes[1] -- use first recipe
local parts = {}
for rId, rData in pairs( rec.reactants or {} ) do
local amt = rData.amount or 1
local name = rData.name or rId
local cat = rData.catalyst and ' <span class="ss-recipe-cat">кат.</span>' or ''
-- Wrap name in clickable span if the reagent exists in our data
local nameHtml
if reagents[rId] then
nameHtml = '<span class="ss-recipe-link" data-target="reagent-' .. rId .. '">' .. name .. '</span>'
else
nameHtml = name
end
parts[#parts + 1] = {
text = '<span class="ss-recipe-amount">' .. amt .. '</span>\194\160' .. nameHtml .. cat,
sort = name
}
end
table.sort( parts, function(a,b) return a.sort < b.sort end )
local inputs = {}
for _, p2 in ipairs( parts ) do
inputs[#inputs + 1] = p2.text
end
local line = table.concat( inputs, ' + ' )
-- Products
local prodParts = {}
for pId, pData in pairs( rec.products or {} ) do
local amt = pData.amount or 1
local name = pData.name or pId
prodParts[#prodParts + 1] = '<span class="ss-recipe-amount">' .. amt
.. '</span>\194\160' .. name
end
if #prodParts > 0 then
line = line .. ' <span class="ss-recipe-arrow">=</span> '
.. table.concat( prodParts, ' + ' )
end
-- Build method tag(s)
local tags = {}
-- Mixer type
local mixer = rec.mixer
if mixer then
for _, m in ipairs( mixer ) do
local label = mixerNames[m] or m
tags[#tags + 1] = '<span class="ss-recipe-method ss-recipe-method--mixer">' .. label .. '</span>'
end
end
-- Temperature conditions
if rec.minTemp and rec.minTemp > 0 then
tags[#tags + 1] = '<span class="ss-recipe-method ss-recipe-method--heat">▲ ' .. rec.minTemp .. ' K</span>'
end
if rec.maxTemp and rec.maxTemp > 0 then
tags[#tags + 1] = '<span class="ss-recipe-method ss-recipe-method--cool">▼ ' .. rec.maxTemp .. ' K</span>'
end
local methodTag = #tags > 0 and table.concat( tags, ' ' ) or nil
return line, methodTag
end
-- Format a stat cell
local function statCell( label, value, wide )
local cls = wide and 'ss-reagent-stat ss-reagent-stat--wide' or 'ss-reagent-stat'
return '<div class="' .. cls .. '">'
.. '<span class="ss-reagent-stat-k">' .. label .. '</span>'
.. '<span class="ss-reagent-stat-v">' .. value .. '</span>'
.. '</div>'
end
-- Render a single reagent card
local function renderCard( id, r )
local color = cssColor( r.color )
local html = '<div class="ss-reagent-card" id="reagent-' .. id .. '"'
if color then
html = html .. ' style="--primary:' .. attr( color ) .. '"'
end
html = html .. '>'
-- head
html = html .. '<div class="ss-reagent-head">'
html = html .. '<span class="ss-reagent-name">' .. ( r.name or id ) .. '</span>'
html = html .. '</div>'
-- recipe (Elements come from dispenser, skip their decomposition recipes)
local recipeLine, methodTag
if r.group == 'Elements' then
recipeLine = 'Хим-раздатчик'
else
recipeLine, methodTag = buildRecipe( r )
end
if recipeLine then
html = html .. '<div class="ss-reagent-recipe">' .. recipeLine .. '</div>'
end
if methodTag then
html = html .. '<div class="ss-reagent-method">' .. methodTag .. '</div>'
end
-- description
if r.desc and r.desc ~= '' then
html = html .. '<div class="ss-reagent-desc">' .. r.desc .. '</div>'
end
-- parse metabolism
local heal, odHeal, odThreshold, rate, special = parseEffects( r.metabolisms )
-- Build effects string
local effectParts = {}
for _, h in ipairs( heal ) do effectParts[#effectParts + 1] = h end
for _, s in ipairs( special ) do effectParts[#effectParts + 1] = s end
local effectStr = #effectParts > 0 and table.concat( effectParts, ' · ' ) or nil
-- Build overdose string
local odParts = {}
for _, h in ipairs( odHeal ) do odParts[#odParts + 1] = h end
local odStr = #odParts > 0 and table.concat( odParts, ' · ' ) or nil
-- Stats bar
local hasStats = rate or odThreshold or effectStr
if hasStats then
html = html .. '<div class="ss-reagent-stats">'
if rate then
html = html .. statCell( 'МЕТАБ.', rate .. 'u/тик', false )
end
if odThreshold then
html = html .. statCell( 'ПЕРЕДОЗ.', odThreshold .. 'u', false )
end
if effectStr then
html = html .. statCell( 'ЭФФЕКТ', effectStr, true )
end
html = html .. '</div>'
end
-- Overdose effects as secondary row
if odStr then
html = html .. '<div class="ss-reagent-od">'
html = html .. '<span class="ss-reagent-stat-k">ПРИ ПЕРЕДОЗ.</span> '
html = html .. odStr
html = html .. '</div>'
end
html = html .. '</div>'
return html
end
-- Collect and sort reagents for a group
local function getGroupEntries( groupId )
local entries = {}
for id, r in pairs( reagents ) do
if r.group == groupId then
entries[#entries + 1] = { id = id, data = r }
end
end
table.sort( entries, function(a, b)
local aHas = a.data.group == 'Elements' or (a.data.recipes ~= nil and a.data.recipes[1] ~= nil)
local bHas = b.data.group == 'Elements' or (b.data.recipes ~= nil and b.data.recipes[1] ~= nil)
if aHas ~= bHas then return aHas end
return (a.data.name or a.id) < (b.data.name or b.id)
end )
return entries
end
-- ── public ──────────────────────────────────────────────────
function p.group( frame )
local args = frame.args
local groupId = mw.text.trim( args[1] or '' )
if groupId == '' then
groupId = mw.text.trim( (frame:getParent().args or {})[1] or '' )
end
if groupId == '' then
return '<span class="error">Reagents: group id required</span>'
end
local entries = getGroupEntries( groupId )
if #entries == 0 then
return '<span class="error">Reagents: no entries for group "' .. mw.text.nowiki( groupId ) .. '"</span>'
end
local cards = {}
for _, e in ipairs( entries ) do
cards[#cards + 1] = renderCard( e.id, e.data )
end
return '<div class="ss-reagent-grid">\n' .. table.concat( cards, '\n' ) .. '\n</div>'
end
function p.card( frame )
local args = frame.args
local id = mw.text.trim( args[1] or '' )
if id == '' then
id = mw.text.trim( (frame:getParent().args or {})[1] or '' )
end
local r = reagents[id]
if r then
return renderCard( id, r )
end
-- Try finding by name
for rId, rData in pairs( reagents ) do
if rData.name == id then
return renderCard( rId, rData )
end
end
return '<span class="error">Reagents: "' .. mw.text.nowiki( id ) .. '" not found</span>'
end
function p.count( frame )
local groupId = mw.text.trim( frame.args[1] or '' )
if groupId == '' then
groupId = mw.text.trim( (frame:getParent().args or {})[1] or '' )
end
local n = 0
for _, r in pairs( reagents ) do
if r.group == groupId then n = n + 1 end
end
return tostring( n )
end
return p