Модуль:Reagents

Версия от 11:13, 17 мая 2026; M 9SCO (обсуждение | вклад) (Новая страница: «-- 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 --...»)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)

Для документации этого модуля может быть создана страница Модуль: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">&#9650; ' .. 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">&#9660; ' .. 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