Модуль:MedicalStats
Для документации этого модуля может быть создана страница Модуль:MedicalStats/doc
-- Модуль:MedicalStats
-- Автоматическая памятка медика: сколько лечит/наносит урона лекарство за 1u,
-- с какой дозы начинается передозировка, что делают бинты, мази, койки и дефибриллятор.
--
-- Все цифры берутся из выгрузки IanComradeBot:
-- prototype/Reagent.json — метаболизм реагентов (эффекты, условия, metabolismRate)
-- component/Healing.json — бинты, мази, наборы (компонент Healing)
-- component/HealOnBuckle.json — кровати и койки (компонент HealOnBuckle)
-- component/Defibrillator.json — дефибрилляторы
-- prototype/DamageType.json, prototype/DamageGroup.json — типы и группы урона
-- loc.json — русская локализация (ru-RU)
-- entity prototypes.json — названия сущностей
--
-- Как считается «за 1u»: раз в секунду (тик метаболизма) из крови убирается
-- metabolismRate единиц реагента (по умолчанию 0.5u) и эффекты применяются один раз.
-- Значит, эффект на 1u = значение эффекта / metabolismRate.
local p = {}
local getArgs = require('Module:Arguments').getArgs
local JsonPaths = require('Module:JsonPaths')
local lang = mw.getContentLanguage()
-- В игре metabolismRate по умолчанию 0.5 (ReagentEffectsEntry.MetabolismRate).
-- Бот не выгружает значения по умолчанию, поэтому оно продублировано здесь.
local DEFAULT_METABOLISM_RATE = 0.5
-- Значения DataField по умолчанию для эффектов, которые бот не выгружает.
local EFFECT_DEFAULTS = {
ModifyBleed = { amount = -1 },
ModifyBloodLevel = { amount = 1 },
CleanBloodstream = { cleanseRate = 3 },
EyeDamage = { amount = -1 },
}
-- Порядок групп урона при выводе (только порядок, сами группы и их типы — из данных бота).
local GROUP_ORDER = { "Brute", "Burn", "Airloss", "Toxin", "Genetic", "Metaphysical" }
---------------------------------------------------------------------------
-- Загрузка данных (mw.loadData кэширует страницу на всё время рендера)
---------------------------------------------------------------------------
local cache = {}
local function load(rel)
if cache[rel] == nil then
local ok, data = pcall(mw.loadData, JsonPaths.get(rel))
cache[rel] = (ok and type(data) == "table") and data or {}
end
return cache[rel]
end
-- Пустая ли таблица. Функция next и оператор # для таблиц из mw.loadData не работают
-- (это прокси только для чтения), поэтому проверяем через pairs.
local function isEmpty(t)
for _ in pairs(t) do
return false
end
return true
end
-- Запись прототипа/компонента из файла формата { default = {...}, id = { Id = {...} } }.
-- Возвращает функцию-геттер поля с подстановкой default.
local function getEntry(rel, id)
local data = load(rel)
local ids = data.id or {}
local entry = ids[id]
if entry == nil then
return nil
end
local default = data.default or {}
return function(field)
local v = entry[field]
-- бот иногда выгружает переопределение пустой таблицей — тогда берём default
if v == nil or (type(v) == "table" and isEmpty(v)) then
v = default[field]
end
return v
end
end
-- Перевод по ключу. Если перевода нет в выгрузке бота, возвращает nil,
-- и вызывающий код показывает ID прототипа вместо ключа вида «metabolizer-type-…».
local function loc(key)
if not key then
return nil
end
local v = load("loc.json")[key]
if type(v) == "table" then
v = v._value
end
return v
end
local function ucfirst(s)
return s and lang:ucfirst(s) or s
end
---------------------------------------------------------------------------
-- Форматирование
---------------------------------------------------------------------------
-- Число с округлением до сотых, десятичная запятая по правилам ru.
local function num(x)
x = math.floor(math.abs(x) * 100 + 0.5) / 100
return lang:formatNum(x)
end
local function good(x)
return '<span style="color:green">' .. num(x) .. '</span>'
end
local function bad(x)
return '<span style="color:red">' .. num(x) .. '</span>'
end
local function typeName(typeId)
local dt = getEntry("prototype/DamageType.json", typeId)
return ucfirst(loc(dt and dt("name"))) or typeId
end
local function groupName(groupId)
local dg = getEntry("prototype/DamageGroup.json", groupId)
return ucfirst(loc(dg and dg("name"))) or groupId
end
local function groupTypes(groupId)
local dg = getEntry("prototype/DamageGroup.json", groupId)
local list = {}
for _, t in ipairs(dg and dg("damageTypes") or {}) do
list[#list + 1] = t
end
return list
end
local function reagentName(id)
local r = getEntry("prototype/Reagent.json", id)
return ucfirst(loc(r and r("name"))) or id
end
local function reagentLink(id)
return "[[Химия#chem_" .. id .. "|" .. reagentName(id) .. "]]"
end
local function entityName(id)
local overrides = load("entity name overrides.json")
if overrides[id] then
return overrides[id]
end
local ent = load("entity prototypes.json")[id]
return ent and ent.name or id
end
---------------------------------------------------------------------------
-- Урон: накопление и вывод
---------------------------------------------------------------------------
-- Порядок ключей урона: группы по GROUP_ORDER, внутри — типы в порядке группы.
local function damageOrder()
if cache._order then
return cache._order
end
local order, n = {}, 0
for _, g in ipairs(GROUP_ORDER) do
n = n + 1
order["g:" .. g] = n
for _, t in ipairs(groupTypes(g)) do
n = n + 1
order["t:" .. t] = n
end
end
cache._order = order
return order
end
-- acc = { list = { key, ... }, val = { key = число } }
local function newAcc()
return { list = {}, val = {} }
end
local function accAdd(acc, key, amount)
if amount == 0 then
return
end
if acc.val[key] == nil then
acc.list[#acc.list + 1] = key
acc.val[key] = 0
end
acc.val[key] = acc.val[key] + amount
end
-- DamageSpecifier: { types = {...}, groups = {...} }. Группа делится поровну между её типами.
local function addSpecifier(acc, spec, k)
if type(spec) ~= "table" then
return
end
for t, v in pairs(spec.types or {}) do
accAdd(acc, "t:" .. t, v * k)
end
for g, v in pairs(spec.groups or {}) do
local types = groupTypes(g)
for _, t in ipairs(types) do
accAdd(acc, "t:" .. t, v * k / #types)
end
end
end
-- Название ключа урона с числом.
-- Тип: «Ушибы 9».
-- Группа (EvenHealthChange): «Механические 3 (по 1 на Ушибы, Порезы, Уколы)» —
-- в игре лечение группы делится поровну между её типами (DamageableSystem.HealEvenly),
-- а если часть типов не повреждена, остаток уходит в повреждённые.
local function keyText(key, v, colorize)
local kind, id = key:match("^(%a):(.+)$")
if kind ~= "g" then
return typeName(id) .. " " .. colorize(v)
end
local types = groupTypes(id)
local text = groupName(id) .. " " .. colorize(v)
if #types > 1 then
local names = {}
for _, t in ipairs(types) do
names[#names + 1] = typeName(t)
end
text = text .. " (по " .. num(v / #types) .. " на " .. table.concat(names, ", ") .. ")"
end
return text
end
-- Возвращает строки «Лечит: …» и «Наносит: …».
local function renderDamage(acc)
local order = damageOrder()
local keys = {}
for _, key in ipairs(acc.list) do
if math.abs(acc.val[key]) >= 0.005 then
keys[#keys + 1] = key
end
end
table.sort(keys, function(a, b)
local oa, ob = order[a] or 1000, order[b] or 1000
if oa ~= ob then
return oa < ob
end
return a < b
end)
local heals, deals = {}, {}
for _, key in ipairs(keys) do
local v = acc.val[key]
if v < 0 then
heals[#heals + 1] = keyText(key, v, good)
else
deals[#deals + 1] = keyText(key, v, bad)
end
end
local out = {}
if #heals > 0 then
out[#out + 1] = "лечит: " .. table.concat(heals, ", ")
end
if #deals > 0 then
out[#out + 1] = "наносит: " .. table.concat(deals, ", ")
end
return out
end
---------------------------------------------------------------------------
-- Реагенты
---------------------------------------------------------------------------
-- Разбор записи эффекта бота: { ["!type:HealthChange"] = { ... } } -> "HealthChange", { ... }
local function unwrap(obj)
for key, val in pairs(obj) do
local t = type(key) == "string" and key:match("^!type:(.+)$")
if t then
return t, val
end
end
return nil, nil
end
-- Старые имена эффектов (до переезда на EntityEffects; так их пока выгружает бот для Goob)
local EFFECT_ALIASES = {
ChemVomit = "Vomit",
ChemHealEyeDamage = "EyeDamage",
ChemCleanBloodstream = "CleanBloodstream",
ModifyBleedAmount = "ModifyBleed",
}
local function field(effType, eff, name)
local v = eff[name]
if v == nil and EFFECT_DEFAULTS[effType] then
v = EFFECT_DEFAULTS[effType][name]
end
return v
end
-- Условия эффекта: { self = {min=, max=}, others = { "текст", ... } }
-- Старые/форковые имена условий -> имена, которые понимает модуль.
-- ReagentThreshold, Temperature, TotalDamage, OrganType — старая схема (так их пока выгружает бот для Goob).
local CONDITION_ALIASES = {
ReagentThreshold = "ReagentCondition",
Temperature = "TemperatureCondition",
TotalDamage = "TotalDamageCondition",
OrganType = "MetabolizerTypeCondition",
}
local function parseConditions(eff, reagentId)
local res = { others = {} }
for _, c in ipairs(eff.conditions or {}) do
local ct, cond = unwrap(c)
ct = CONDITION_ALIASES[ct] or ct
cond = cond or {}
local inv = cond.inverted
if ct == "ReagentCondition" and cond.reagent == nil then
-- в ReagentThreshold реагент по умолчанию — сам метаболизируемый реагент
cond = { reagent = reagentId, min = cond.min, max = cond.max, inverted = cond.inverted }
end
if ct == "ReagentCondition" and cond.reagent == reagentId and not inv then
if cond.min and cond.min > 0 then
res.selfMin = cond.min
end
if cond.max then
res.selfMax = cond.max
end
elseif ct == "ReagentCondition" then
local what = reagentLink(cond.reagent)
local text
if cond.min and cond.min > 1 then
text = "если в крови от " .. num(cond.min) .. "u вещества " .. what
else
text = "если в крови есть " .. what
end
if cond.max then
text = text .. " (не больше " .. num(cond.max) .. "u)"
end
res.others[#res.others + 1] = inv and ("кроме случая: " .. text) or text
elseif ct == "MobStateCondition" then
local state = cond.mobstate or "Alive"
local names = {
Alive = "не в критическом состоянии",
Critical = "в критическом состоянии",
Dead = "у мёртвых",
}
local s = names[state] or state
res.others[#res.others + 1] = (inv and "только не " or "только ") .. s
elseif ct == "TemperatureCondition" then
local parts = {}
if cond.min then
parts[#parts + 1] = "выше " .. num(cond.min) .. " K (" .. (cond.min < 273.15 and "−" or "") .. num(cond.min - 273.15) .. " °C)"
end
if cond.max then
parts[#parts + 1] = "ниже " .. num(cond.max) .. " K (" .. (cond.max < 273.15 and "−" or "") .. num(cond.max - 273.15) .. " °C)"
end
res.others[#res.others + 1] = "при температуре тела " .. table.concat(parts, " и ")
elseif ct == "TotalDamageCondition" then
local parts = {}
if cond.min then
parts[#parts + 1] = "от " .. num(cond.min)
end
if cond.max then
parts[#parts + 1] = "до " .. num(cond.max)
end
res.others[#res.others + 1] = (inv and "кроме случая: " or "") .. "при общем уроне " .. table.concat(parts, " ")
elseif ct == "MetabolizerTypeCondition" then
-- Исключения для отдельных рас (например, «кроме Yowie» в Goob) в памятке не показываем,
-- а «только для …» показываем.
local excluded = inv or cond.shouldHave == false
if not excluded then
local types = cond.type
if type(types) ~= "table" then
types = { types }
end
local names = {}
for _, t in ipairs(types) do
local mt = getEntry("prototype/MetabolizerType.json", t)
names[#names + 1] = ucfirst(loc(mt and mt("name"))) or t
end
res.others[#res.others + 1] = "только для метаболизма: " .. table.concat(names, ", ")
end
elseif ct then
res.others[#res.others + 1] = "при особом условии (" .. ct .. ")"
end
end
return res
end
-- Текст «прочих» (не уронных) эффектов. k — множитель «за 1u».
local function otherEffectText(effType, eff, k, reagentId)
if effType == "ModifyBleed" then
local a = field(effType, eff, "amount") * k
if a < 0 then
return "уменьшает кровотечение на " .. good(a)
end
return "усиливает кровотечение на " .. bad(a)
elseif effType == "ModifyBloodLevel" then
local a = field(effType, eff, "amount") * k
if a > 0 then
return "восполняет кровь на " .. good(a) .. "u"
end
return "уменьшает объём крови на " .. bad(a) .. "u"
elseif effType == "CleanBloodstream" then
local a = field(effType, eff, "cleanseRate") * k
return "выводит из крови по " .. good(a) .. "u остальных веществ"
elseif effType == "AdjustReagent" then
local a = (eff.amount or 0) * k
if eff.reagent == reagentId then
return nil -- сам себя выводит/добавляет — для памятки не важно
end
if a < 0 then
return "выводит " .. good(a) .. "u вещества " .. reagentLink(eff.reagent)
end
return "добавляет " .. bad(a) .. "u вещества " .. reagentLink(eff.reagent)
elseif effType == "EyeDamage" then
local a = field(effType, eff, "amount") * k
if a < 0 then
return "лечит повреждение глаз на " .. good(a)
end
return "повреждает глаза на " .. bad(a)
elseif effType == "CureZombieInfection" then
if eff.innoculate then
return "излечивает [[зомби]]-вирус и даёт иммунитет к нему"
end
return "излечивает [[зомби]]-вирус"
elseif effType == "AdjustTemperature" then
return (eff.amount or 0) < 0 and "охлаждает тело" or "нагревает тело"
elseif effType == "ModifyKnockdown" or effType == "ModifyStatusEffect" or effType == "ModifyParalysis" then
if eff.type == "Remove" then
return "сокращает оглушение и сбивание с ног"
end
return nil
elseif effType == "SuppressPain" then
return "подавляет боль"
elseif effType == "Vomit" then
return "рвота"
elseif effType == "Jitter" then
return "дрожь"
elseif effType == "Drunk" then
return "опьянение"
end
-- PopupMessage, Emote, SatiateHunger и т.п. в памятке не показываем
return nil
end
local function isDamageEffect(effType)
return effType == "HealthChange" or effType == "EvenHealthChange"
end
-- Есть ли у уронного эффекта положительный (вредный) урон
local function hasHarm(effType, eff)
local d = eff.damage or {}
if effType == "EvenHealthChange" then
for _, v in pairs(d) do
if v > 0 then return true end
end
return false
end
for _, part in pairs({ d.types or {}, d.groups or {} }) do
for _, v in pairs(part) do
if v > 0 then return true end
end
end
return false
end
-- Эффекты, которые при пороговой дозе являются полезными (а не передозировкой)
local BENEFICIAL = { CureZombieInfection = true, SuppressPain = true, EyeDamage = true, ModifyBleed = true, ModifyBloodLevel = true }
-- Группа эффектов с одинаковыми условиями: { label = "…", acc = урон, extra = { "…" } }
local function bucketFor(buckets, label)
for _, b in ipairs(buckets) do
if b.label == label then
return b
end
end
local b = { label = label, acc = newAcc(), extra = {} }
buckets[#buckets + 1] = b
return b
end
local function addExtra(b, text)
if not text then return end
for _, e in ipairs(b.extra) do
if e == text then return end
end
b.extra[#b.extra + 1] = text
end
local function renderBucket(b)
local parts = renderDamage(b.acc)
for _, e in ipairs(b.extra) do
parts[#parts + 1] = e
end
if #parts == 0 then
return nil
end
local text = table.concat(parts, "; ")
if b.label ~= "" then
return ucfirst(b.label) .. ": " .. text .. "."
end
return ucfirst(text) .. "."
end
-- Метаболизм реагента: Bloodstream, иначе первый найденный (например, Digestion у напитков)
local function pickMetabolism(metabolisms)
if type(metabolisms) ~= "table" then
return nil
end
-- Bloodstream — Corvax/апстрим, Medicine — группа метаболизма лекарств в Goob
for _, key in ipairs({ "Bloodstream", "Medicine" }) do
if metabolisms[key] then
return metabolisms[key]
end
end
local firstKey
for key in pairs(metabolisms) do
if not firstKey or key < firstKey then
firstKey = key
end
end
return firstKey and metabolisms[firstKey]
end
-- Полный разбор реагента
local function analyzeReagent(id)
local r = getEntry("prototype/Reagent.json", id)
if not r then
return nil
end
local m = pickMetabolism(r("metabolisms"))
local res = { rate = DEFAULT_METABOLISM_RATE, normal = {}, overdose = {}, threshold = nil }
if not m then
return res
end
res.rate = m.metabolismRate or DEFAULT_METABOLISM_RATE
local k = 1 / res.rate
bucketFor(res.normal, "") -- безусловные эффекты всегда первыми
local harmThreshold, anyThreshold
for _, wrapped in ipairs(m.effects or {}) do
local effType, eff = unwrap(wrapped)
effType = EFFECT_ALIASES[effType] or effType
if effType and eff then
local cond = parseConditions(eff, id)
local label = table.concat(cond.others, ", ")
if cond.selfMax then
label = (label ~= "" and (label .. ", ") or "") .. "пока в крови не больше " .. num(cond.selfMax) .. "u"
end
if eff.probability and eff.probability < 1 then
label = (label ~= "" and (label .. ", ") or "") .. "с шансом " .. num(eff.probability * 100) .. "% в секунду"
end
local isOverdose = false
if cond.selfMin then
if isDamageEffect(effType) then
isOverdose = hasHarm(effType, eff)
else
isOverdose = not BENEFICIAL[effType]
end
if not isOverdose then
label = (label ~= "" and (label .. ", ") or "") .. "от " .. num(cond.selfMin) .. "u в крови"
end
end
local b
if isOverdose then
local odLabel = "от " .. num(cond.selfMin) .. "u" .. (label ~= "" and (", " .. label) or "")
b = bucketFor(res.overdose, odLabel)
b.min = cond.selfMin
if isDamageEffect(effType) and (not harmThreshold or cond.selfMin < harmThreshold) then
harmThreshold = cond.selfMin
end
if not anyThreshold or cond.selfMin < anyThreshold then
anyThreshold = cond.selfMin
end
else
b = bucketFor(res.normal, label)
end
-- Передозировка показывается «в секунду» (пока доза выше порога), остальное — «за 1u»
local mult = isOverdose and 1 or k
if effType == "HealthChange" then
addSpecifier(b.acc, eff.damage, mult)
elseif effType == "EvenHealthChange" then
for g, v in pairs(eff.damage or {}) do
accAdd(b.acc, "g:" .. g, v * mult)
end
else
addExtra(b, otherEffectText(effType, eff, mult, id))
end
end
end
table.sort(res.overdose, function(a, b) return a.min < b.min end)
res.threshold = harmThreshold or anyThreshold
return res
end
local function reagentEffectsText(id, a)
local lines = {}
for _, b in ipairs(a.normal) do
local t = renderBucket(b)
if t then
lines[#lines + 1] = t
end
end
if #a.overdose > 0 then
local od = {}
for _, b in ipairs(a.overdose) do
local t = renderBucket(b)
if t then
od[#od + 1] = "<br>" .. t
end
end
if #od > 0 then
lines[#lines + 1] = "<small>'''Передозировка''' (каждую секунду, пока доза выше порога):" .. table.concat(od) .. "</small>"
end
end
return table.concat(lines, "<br>")
end
local function spriteCell(img, label)
return '! <span class=sprite>[[Файл:' .. img .. '|64px|центр]]</span> <center>' .. label .. '</center>'
end
---------------------------------------------------------------------------
-- Публичные функции
---------------------------------------------------------------------------
-- Строка таблицы памятки для реагента.
-- {{#invoke:MedicalStats|reagent|Bicaridine|img=bicaridine.png|note=…}}
function p.reagent(frame)
local args = getArgs(frame)
local id = mw.text.trim(args[1] or "")
local a = analyzeReagent(id)
if not a then
return "|-\n| colspan=\"3\" | <span class=\"error\">Реагент «" .. id .. "» не найден в выгрузке бота</span>"
end
local dose = a.threshold and (num(a.threshold) .. "u") or "Нет"
local text = reagentEffectsText(id, a)
text = (text ~= "" and (text .. "<br>") or "") .. "<small>Усваивается по " .. num(a.rate) .. "u в секунду.</small>"
if args.note then
text = text .. "<br>" .. args.note
end
return table.concat({
"|-",
spriteCell(args.img or (id .. ".png"), "[[Химия#chem_" .. id .. "|" .. (args.name or reagentName(id)) .. "]]"),
"| <center>" .. dose .. "</center>",
"| " .. text,
}, "\n")
end
-- Только порог передозировки: {{#invoke:MedicalStats|overdose|Bicaridine}} -> 15
function p.overdose(frame)
local args = getArgs(frame)
local a = analyzeReagent(mw.text.trim(args[1] or ""))
return (a and a.threshold) and num(a.threshold) or ""
end
-- Только описание эффектов реагента: {{#invoke:MedicalStats|reagentEffects|Bicaridine}}
function p.reagentEffects(frame)
local args = getArgs(frame)
local id = mw.text.trim(args[1] or "")
local a = analyzeReagent(id)
return a and reagentEffectsText(id, a) or ""
end
-- Строка таблицы для предмета с компонентом Healing (бинты, мази, наборы, пакет крови).
-- {{#invoke:MedicalStats|healing|Brutepack|img=Brutepack.png}}
function p.healing(frame)
local args = getArgs(frame)
local id = mw.text.trim(args[1] or "")
local h = getEntry("component/Healing.json", id)
if not h then
return "|-\n| colspan=\"3\" | <span class=\"error\">У «" .. id .. "» нет компонента Healing в выгрузке бота</span>"
end
local acc = newAcc()
addSpecifier(acc, h("damage"), 1)
local parts = renderDamage(acc)
local bleed = h("bloodlossModifier") or 0
if bleed < 0 then
parts[#parts + 1] = "уменьшает кровотечение на " .. good(bleed)
elseif bleed > 0 then
parts[#parts + 1] = "усиливает кровотечение на " .. bad(bleed)
end
local blood = h("modifyBloodLevel") or 0
-- В Goob знак инвертирован: HealingSystem вызывает TryModifyBloodLevel(-ModifyBloodLevel)
-- (Goob-Station, Content.Shared/Medical/Healing/HealingSystem.cs, «Goobedit»).
if JsonPaths.project() == "Goob" then
blood = -blood
end
if blood > 0 then
parts[#parts + 1] = "восполняет кровь на " .. good(blood) .. "u"
elseif blood < 0 then
parts[#parts + 1] = "уменьшает объём крови на " .. bad(blood) .. "u"
end
local text = ucfirst(table.concat(parts, "; ")) .. "."
local delay = h("delay")
if delay then
text = text .. "<br><small>Наложение занимает " .. num(delay) .. " с"
.. (((h("selfHealPenaltyMultiplier") or 0) > 1) and ", на себе — дольше" or "") .. ".</small>"
end
if args.note then
text = text .. "<br>" .. args.note
end
return table.concat({
"|-",
spriteCell(args.img or (id .. ".png"), args.name or entityName(id)),
"| <center>Нет</center>",
"| " .. text,
}, "\n")
end
-- Строка таблицы для дефибриллятора.
-- {{#invoke:MedicalStats|defib|Defibrillator|img=Defib.png|note=…}}
function p.defib(frame)
local args = getArgs(frame)
local id = mw.text.trim(args[1] or "Defibrillator")
local d = getEntry("component/Defibrillator.json", id)
if not d then
return "|-\n| colspan=\"3\" | <span class=\"error\">У «" .. id .. "» нет компонента Defibrillator в выгрузке бота</span>"
end
local acc = newAcc()
addSpecifier(acc, d("zapHeal"), 1)
local parts = renderDamage(acc)
local zap = d("zapDamage")
if zap and zap > 0 then
parts[#parts + 1] = "бьёт током на " .. bad(zap) .. " урона"
end
local text = "За разряд " .. table.concat(parts, "; ") .. "."
if args.note then
text = text .. "<br>" .. args.note
end
return table.concat({
"|-",
spriteCell(args.img or (id .. ".png"), args.name or entityName(id)),
"| <center>Нет</center>",
"| " .. text,
}, "\n")
end
-- Текст лечения кроватью/койкой (компонент HealOnBuckle).
-- {{#invoke:MedicalStats|bed|MedicalBed}} -> «Лечит: Ушибы 0,2, … каждую секунду»
function p.bed(frame)
local args = getArgs(frame)
local id = mw.text.trim(args[1] or "")
local b = getEntry("component/HealOnBuckle.json", id)
if not b then
return '<span class="error">У «' .. id .. '» нет компонента HealOnBuckle в выгрузке бота</span>'
end
local acc = newAcc()
addSpecifier(acc, b("damage"), 1)
local parts = renderDamage(acc)
if #parts == 0 then
return "Не лечит."
end
local period = b("healTime") or 1
local text = ucfirst(table.concat(parts, "; "))
.. (period == 1 and " каждую секунду" or (" каждые " .. num(period) .. " с"))
local sleep = b("sleepMultiplier")
if sleep and sleep ~= 1 then
text = text .. ", во сне — в " .. num(sleep) .. " раза больше"
end
return text .. "."
end
-- Множитель стазис-кровати: {{#invoke:MedicalStats|stasis|StasisBed}} -> 10
function p.stasis(frame)
local args = getArgs(frame)
local s = getEntry("component/StasisBed.json", mw.text.trim(args[1] or "StasisBed"))
return s and num(s("multiplier") or 0) or ""
end
---------------------------------------------------------------------------
-- Базовые показатели организма (для текста страницы)
---------------------------------------------------------------------------
-- Порог состояния: {{#invoke:MedicalStats|threshold|MobHuman|Critical}} -> 100.
-- Если такого состояния у сущности нет (например, PreCritical вне Wega) — пустая строка,
-- поэтому вызов можно оборачивать в {{#if:}}.
function p.threshold(frame)
local args = getArgs(frame)
local t = getEntry("component/MobThresholds.json", mw.text.trim(args[1] or "MobHuman"))
local state = mw.text.trim(args[2] or "Critical")
if not t then
return ""
end
for dmg, st in pairs(t("thresholds") or {}) do
if st == state then
return num(tonumber(dmg))
end
end
return ""
end
-- Нормальный объём крови в u. В новой схеме — bloodReferenceSolution, в старой (Goob) — bloodMaxVolume.
local function bloodVolume(id)
local b = getEntry("component/Bloodstream.json", id)
if not b then
return nil, nil
end
local ref = b("bloodReferenceSolution")
local vol = type(ref) == "table" and ref.maxVol or nil
if not vol and type(ref) == "table" then
for _, r in ipairs(ref.reagents or {}) do
vol = (vol or 0) + (r.Quantity or 0)
end
end
vol = vol or b("bloodMaxVolume")
return vol, b
end
-- {{#invoke:MedicalStats|blood|MobHuman}} -> 600 (норма крови)
-- {{#invoke:MedicalStats|blood|MobHuman|pale}} -> 540 (ниже — «выглядит бледно» и урон от кровопотери)
-- {{#invoke:MedicalStats|blood|MobHuman|palePercent}} -> 90
-- {{#invoke:MedicalStats|blood|MobHuman|maxBleed}} -> 10
function p.blood(frame)
local args = getArgs(frame)
local vol, b = bloodVolume(mw.text.trim(args[1] or "MobHuman"))
if not b then
return ""
end
local mode = mw.text.trim(args[2] or "volume")
local thr = b("bloodlossThreshold") or 0.9
if mode == "pale" then
return vol and num(vol * thr) or ""
elseif mode == "palePercent" then
return num(thr * 100)
elseif mode == "maxBleed" then
return num(b("maxBleedAmount") or 10)
end
return vol and num(vol) or ""
end
-- Сколько разных веществ орган обрабатывает за тик: {{#invoke:MedicalStats|organLimit|OrganHumanHeart}} -> 2.
-- В игре по умолчанию 3 (MetabolizerComponent.MaxReagentsProcessable); бот дефолт не всегда выгружает.
function p.organLimit(frame)
local args = getArgs(frame)
local m = getEntry("component/Metabolizer.json", mw.text.trim(args[1] or "OrganHumanHeart"))
if not m then
return ""
end
return num(m("maxReagents") or 3)
end
-- Задержка переваривания желудка в секундах (есть только в старой схеме, например Goob).
-- {{#invoke:MedicalStats|digestionDelay|OrganHumanStomach}} -> 20 или пустая строка.
function p.digestionDelay(frame)
local args = getArgs(frame)
local st = getEntry("component/Stomach.json", mw.text.trim(args[1] or "OrganHumanStomach"))
local d = st and st("digestionDelay")
return d and num(d) or ""
end
return p