Модуль:SpeciesStats: различия между версиями

Нет описания правки
Нет описания правки
(не показаны 4 промежуточные версии этого же участника)
Строка 25: Строка 25:
--  {{#invoke:SpeciesStats|features|Arachnid|header=Особенности|notes=...}} — особенности (ручные строки + из данных)
--  {{#invoke:SpeciesStats|features|Arachnid|header=Особенности|notes=...}} — особенности (ручные строки + из данных)
--  {{#invoke:SpeciesStats|missing|Human, Arachnid, ...}} — расы проекта, которых нет в списке
--  {{#invoke:SpeciesStats|missing|Human, Arachnid, ...}} — расы проекта, которых нет в списке
--  {{#invoke:SpeciesStats|image|Arachnid}}  — имя файла картинки расы (для шторки)
--  {{#invoke:SpeciesStats|nav|Human:Люди, ...}}    — сетка иконок рас (сейчас на странице не используется)
--  {{#invoke:SpeciesStats|summary|Human:Люди, ...}} — сводная таблица рас (сейчас не используется)
--
--
-- КАК ПРАВИТЬ
-- КАК ПРАВИТЬ
Строка 39: Строка 42:
--  * Особенность, которой нет в данных, пишется не здесь, а на самой странице
--  * Особенность, которой нет в данных, пишется не здесь, а на самой странице
--    в параметре notes= у нужной расы.
--    в параметре notes= у нужной расы.
--  * Расцветка: зелёный — положительное (лечение, преимущества), красный —
--    отрицательное (урон, недостатки). Для урона используйте takenText(), для лечения —
--    healText(), для фраз — tone(). Режим сравнения с человеком — COMPARE_WITH_HUMAN.
--  * Порядок групп урона и органов задают GROUP_ORDER и ORGAN_ORDER ниже.
--  * Порядок групп урона и органов задают GROUP_ORDER и ORGAN_ORDER ниже.
--    Состав групп и органов там не задаётся: его дают данные.
--    Состав групп и органов там не задаётся: его дают данные.
Строка 224: Строка 230:
end
end
return out
return out
end
-- Цвет значений, которые совпадают с человеческими (нейтральный жёлтый).
-- Расцветка страницы:
--  зелёный (class="positive") — положительное: лечение, преимущества, баффы;
--  красный (class="negative") — отрицательное: получаемый урон, недостатки, дебаффы.
--  жёлтый (SAME_STYLE) — остальные числа (здоровье, скорость, температура и т. п.).
-- COMPARE_WITH_HUMAN = true включает другой режим: числа красятся по сравнению с человеком
-- (лучше — зелёный, хуже — красный, так же — SAME_STYLE). Сейчас выключен.
local COMPARE_WITH_HUMAN = false
-- Нейтральный цвет чисел.
local SAME_STYLE = "color:#e6c07b;font-weight:bold"
-- true, пока выводится сам человек: сравнивать его с собой незачем, значения без цвета.
local isBaseline = false
-- Подсветка значения в сравнении с человеком — единая расцветка страницы:
--  зелёный (class="positive") — лучше, чем у человека;
--  красный (class="negative") — хуже, чем у человека;
--  жёлтый (SAME_STYLE) — так же, как у человека;
--  без цвета — сравнивать не с чем.
-- При наведении на подсвеченное значение видно значение человека.
-- higherBetter: true — чем больше, тем лучше (скорость, здоровье), false — наоборот (урон).
-- fmt — как показать значение человека в подсказке (по умолчанию num).
local function mark(text, v, base, higherBetter, fmt)
if not COMPARE_WITH_HUMAN then
-- Обычное число (не урон и не лечение): нейтральный цвет, для красоты.
if type(v) == "number" then
return '<span style="' .. SAME_STYLE .. '">' .. text .. '</span>'
end
return text
end
if type(v) ~= "number" or type(base) ~= "number" then
return text
end
if isBaseline then
return text
end
if math.abs(v - base) < 1e-6 then
return '<span style="' .. SAME_STYLE .. '" title="Как у человека">' .. text .. '</span>'
end
local better = (v > base) == higherBetter
local title = base == 0 and "У человека нет" or ("У человека: " .. (fmt or num)(base))
return '<span class="' .. (better and "positive" or "negative") .. '" title="' .. title .. '">' .. text .. '</span>'
end
-- Подсветка целой фразы: good = true — зелёная, false — красная.
local function tone(text, good)
return '<span class="' .. (good and "positive" or "negative") .. '">' .. text .. '</span>'
end
end


-- DamageSpecifier ({ types = {...}, groups = {...} }) → «уколы 5, ушибы 2».
-- DamageSpecifier ({ types = {...}, groups = {...} }) → «уколы 5, ушибы 2».
local function damageText(spec)
-- base — такой же DamageSpecifier человека для сравнения (тип, которого у человека нет,
-- считается равным 0); nil — без подсветки. higherBetter — как в mark().
local function damageText(spec, base, higherBetter)
if type(spec) ~= "table" then
if type(spec) ~= "table" then
return nil
return nil
end
end
local function value(v, b)
if base and COMPARE_WITH_HUMAN then
return mark(num(v), v, b or 0, higherBetter)
elseif base then
-- Без сравнения: урон, который получает раса, — красный, лечение — зелёный.
return tone(num(v), higherBetter)
end
return num(v)
end
local baseTypes = type(base) == "table" and base.types or {}
local baseGroups = type(base) == "table" and base.groups or {}
local parts = {}
local parts = {}
for _, e in ipairs(orderedTypes(spec.types or {})) do
for _, e in ipairs(orderedTypes(spec.types or {})) do
if e[2] ~= 0 then
if e[2] ~= 0 then
parts[#parts + 1] = mw.ustring.lower(typeName(e[1])) .. " " .. num(e[2])
parts[#parts + 1] = mw.ustring.lower(typeName(e[1])) .. " " .. value(e[2], baseTypes[e[1]])
end
end
end
end
Строка 240: Строка 307:
if v ~= 0 then
if v ~= 0 then
local dg = getEntry("prototype/DamageGroup.json", gid)
local dg = getEntry("prototype/DamageGroup.json", gid)
parts[#parts + 1] = mw.ustring.lower(loc(dg and dg("name")) or gid) .. " " .. num(v)
parts[#parts + 1] = mw.ustring.lower(loc(dg and dg("name")) or gid) .. " " .. value(v, baseGroups[gid])
end
end
end
end
Строка 247: Строка 314:
end
end
return table.concat(parts, ", ")
return table.concat(parts, ", ")
end
-- Урон, который получает раса: подсветка относительно человека (меньше — лучше).
-- base == nil — у человека такого урона нет (считается 0).
local function takenText(spec, base)
return damageText(spec, type(base) == "table" and base or {}, false)
end
end


Строка 264: Строка 337:


-- Отрицательные значения DamageSpecifier как лечение: «ушибы 0,05».
-- Отрицательные значения DamageSpecifier как лечение: «ушибы 0,05».
local function healText(spec)
-- base — лечение человека для сравнения (больше — лучше); nil — у человека его нет.
if type(spec) ~= "table" then
local function healText(spec, base)
return nil
local function negOnly(sp)
end
local neg = { types = {}, groups = {} }
local neg = { types = {}, groups = {} }
local any = false
local any = false
if type(sp) == "table" then
for t, v in pairs(spec.types or {}) do
for t, v in pairs(sp.types or {}) do
if v < 0 then
if v < 0 then
neg.types[t] = -v
neg.types[t] = -v
any = true
any = true
end
end
for g, v in pairs(sp.groups or {}) do
if v < 0 then
neg.groups[g] = -v
any = true
end
end
end
end
return neg, any
end
end
for g, v in pairs(spec.groups or {}) do
local neg, any = negOnly(spec)
if v < 0 then
if not any then
neg.groups[g] = -v
return nil
any = true
end
end
end
return any and damageText(neg) or nil
return damageText(neg, (negOnly(base)), true)
end
end


Строка 696: Строка 776:
for _, e in ipairs(orderedTypes(set("flatReductions") or {})) do
for _, e in ipairs(orderedTypes(set("flatReductions") or {})) do
if e[2] ~= 0 then
if e[2] ~= 0 then
flat[#flat + 1] = ":" .. typeName(e[1]) .. ": на " .. num(e[2]) .. " меньше с каждого удара"
-- Плоское снижение: положительное — урона меньше (зелёный), отрицательное — больше (красный).
local cls = e[2] > 0 and "positive" or "negative"
local sign = e[2] > 0 and "−" or "+"
flat[#flat + 1] = ':<span class="' .. cls .. '">' .. sign .. num(math.abs(e[2])) .. "</span> "
.. typeName(e[1]) .. " с каждого удара"
end
end
end
end
Строка 734: Строка 818:
end
end


-- Список «Основная информация».
-- Пороги состояний моба: { PreCritical = 100, Critical = 125, Dead = 200 }.
local function thresholdsOf(mob)
local thr = mob and comp("MobThresholds", mob)
local th = thr and thr("thresholds")
local out = {}
if not isEmpty(th) then
for _, k in ipairs(sortedNumKeys(th)) do
if not out[th[k]] then
out[th[k]] = tonumber(k)
end
end
end
return out, th
end
 
-- Объём крови моба (новая и старая схема Bloodstream).
local function bloodOf(bs)
if not bs then
return nil, nil
end
local ref = bs("bloodReferenceSolution")
if type(ref) == "table" and not isEmpty(ref.reagents) then
local r = ref.reagents[1]
return r.ReagentId, r.Quantity
end
return bs("bloodReagent"), bs("bloodMaxVolume")
end
 
-- Список «Основная информация». Значения сравниваются с человеком (см. mark).
local function basicLines(id, mob)
local function basicLines(id, mob)
local lines = {}
local lines = {}
local baseMob = speciesMob(BASELINE_SPECIES)
local function bcomp(name)
return baseMob and comp(name, baseMob)
end


-- Пороги состояний
-- Пороги состояний: чем позже наступает состояние, тем лучше.
local thr = comp("MobThresholds", mob)
local base = thresholdsOf(baseMob)
local th = thr and thr("thresholds")
local _, th = thresholdsOf(mob)
if not isEmpty(th) then
if not isEmpty(th) then
local parts = {}
local parts = {}
Строка 746: Строка 862:
for _, k in ipairs(sortedNumKeys(th)) do
for _, k in ipairs(sortedNumKeys(th)) do
local state = th[k]
local state = th[k]
local v = mark(num(k), tonumber(k), base[state], true)
if state == "PreCritical" then
if state == "PreCritical" then
parts[#parts + 1] = "подкрит — с " .. num(k) .. " урона"
parts[#parts + 1] = "подкрит — с " .. v .. " урона"
elseif state == "Critical" then
elseif state == "Critical" then
parts[#parts + 1] = "критическое состояние — с " .. num(k) .. " урона"
parts[#parts + 1] = "критическое состояние — с " .. v .. " урона"
hasCrit = true
hasCrit = true
elseif state == "Dead" then
elseif state == "Dead" then
parts[#parts + 1] = "смерть — с " .. num(k) .. " урона"
parts[#parts + 1] = "смерть — с " .. v .. " урона"
break
break
end
end
Строка 758: Строка 875:
local line = "* '''Здоровье''': " .. table.concat(parts, ", ") .. "."
local line = "* '''Здоровье''': " .. table.concat(parts, ", ") .. "."
if not hasCrit then
if not hasCrit then
line = line .. " Критического состояния нет."
line = line .. " " .. tone("Критического состояния нет.", false)
end
end
lines[#lines + 1] = line
lines[#lines + 1] = line
end
end


-- Скорость
-- Скорость: больше — лучше.
local ms = comp("MovementSpeedModifier", mob)
local ms = comp("MovementSpeedModifier", mob)
if ms then
if ms then
lines[#lines + 1] = "* '''Скорость передвижения''': шаг " .. num(ms("baseWalkSpeed") or 0)
local baseMs = bcomp("MovementSpeedModifier")
.. ", бег " .. num(ms("baseSprintSpeed") or 0) .. "."
local walk, run = ms("baseWalkSpeed") or 0, ms("baseSprintSpeed") or 0
local baseMs = comp("MovementSpeedModifier", speciesMob(BASELINE_SPECIES))
lines[#lines + 1] = "* '''Скорость передвижения''': шаг "
.. mark(num(walk), walk, baseMs and baseMs("baseWalkSpeed"), true)
.. ", бег " .. mark(num(run), run, baseMs and baseMs("baseSprintSpeed"), true) .. "."
local acc = ms("baseWeightlessAcceleration")
local acc = ms("baseWeightlessAcceleration")
local baseAcc = baseMs and baseMs("baseWeightlessAcceleration")
local baseAcc = baseMs and baseMs("baseWeightlessAcceleration")
if acc and baseAcc and baseAcc > 0 and math.abs(acc / baseAcc - 1) > 0.01 then
if acc and baseAcc and baseAcc > 0 and math.abs(acc / baseAcc - 1) > 0.01 then
lines[#lines + 1] = "** Ускорение в невесомости: ×" .. num(acc / baseAcc) .. " от человеческого."
lines[#lines + 1] = "** Ускорение в невесомости: "
.. mark("×" .. num(acc / baseAcc), acc, baseAcc, true) .. " от человеческого."
end
end
end
end


-- Замедление от урона
-- Замедление от урона: множитель скорости, больше — лучше.
local slow = comp("SlowOnDamage", mob)
local slow = comp("SlowOnDamage", mob)
local st = slow and slow("speedModifierThresholds")
local st = slow and slow("speedModifierThresholds")
if not isEmpty(st) then
if not isEmpty(st) then
local baseSlow = bcomp("SlowOnDamage")
local bst = baseSlow and baseSlow("speedModifierThresholds") or {}
local parts = {}
local parts = {}
for _, k in ipairs(sortedNumKeys(st)) do
for _, k in ipairs(sortedNumKeys(st)) do
parts[#parts + 1] = "с " .. num(k) .. " урона — ×" .. num(st[k])
-- Множитель скорости человека при том же уроне: последний его порог не выше k
-- (до первого порога человек не замедлен, множитель 1).
local b, bestKey = 1, nil
for bk, bv in pairs(bst) do
local n = tonumber(bk)
if n and n <= tonumber(k) and (not bestKey or n > bestKey) then
b, bestKey = bv, n
end
end
parts[#parts + 1] = "с " .. num(k) .. " урона — " .. mark("×" .. num(st[k]), st[k], b, true)
end
end
lines[#lines + 1] = "* '''Замедление от урона''': " .. table.concat(parts, ", ") .. "."
lines[#lines + 1] = "* '''Замедление от урона''': " .. table.concat(parts, ", ") .. "."
Строка 789: Строка 920:
-- Кровь
-- Кровь
local bs = comp("Bloodstream", mob)
local bs = comp("Bloodstream", mob)
local bbs = bcomp("Bloodstream")
local _, baseVolume = bloodOf(bbs)
if not bs then
if not bs then
lines[#lines + 1] = "* '''Крови нет'''."
lines[#lines + 1] = "* '''Крови нет'''."
else
else
local reagent, volume
local reagent, volume = bloodOf(bs)
local ref = bs("bloodReferenceSolution")
if type(ref) == "table" and not isEmpty(ref.reagents) then
local r = ref.reagents[1]
reagent, volume = r.ReagentId, r.Quantity
else
reagent, volume = bs("bloodReagent"), bs("bloodMaxVolume")
end
local line = "* '''Кровь''': " .. (reagent and reagentName(reagent) or "?")
local line = "* '''Кровь''': " .. (reagent and reagentName(reagent) or "?")
if volume == 0 then
if volume == 0 then
line = "* '''Крови нет''' (объём крови 0)"
line = "* '''Крови нет''' (объём крови 0)"
elseif volume then
elseif volume then
line = line .. ", " .. num(volume) .. " ед."
line = line .. ", " .. mark(num(volume), volume, baseVolume, true) .. " ед."
end
end
lines[#lines + 1] = line
lines[#lines + 1] = line
local loss = damageText(bs("bloodlossDamage"))
local loss = takenText(bs("bloodlossDamage"), bbs and bbs("bloodlossDamage"))
if loss then
if loss then
lines[#lines + 1] = "** При нехватке крови получает урон (" .. loss .. ") за цикл кровотока"
lines[#lines + 1] = "** При нехватке крови получает урон (" .. loss .. ") за цикл кровотока"
Строка 813: Строка 939:
end
end
if bs("maxBleedAmount") == 0 then
if bs("maxBleedAmount") == 0 then
lines[#lines + 1] = "** Не истекает кровью."
lines[#lines + 1] = "** " .. tone("Не истекает кровью.", true)
elseif bs("bleedReductionAmount") == 0 then
elseif bs("bleedReductionAmount") == 0 then
lines[#lines + 1] = "** Кровотечение само не останавливается."
lines[#lines + 1] = "** " .. tone("Кровотечение само не останавливается.", false)
end
end
if bs("bloodRefreshAmount") == 0 and volume ~= 0 then
if bs("bloodRefreshAmount") == 0 and volume ~= 0 then
lines[#lines + 1] = "** Кровь сама не восстанавливается."
lines[#lines + 1] = "** " .. tone("Кровь сама не восстанавливается.", false)
end
end
end
end


-- Голод и жажда
-- Голод и жажда: медленнее — лучше.
local baseMob = speciesMob(BASELINE_SPECIES)
for _, kind in ipairs({ "Hunger", "Thirst" }) do
for _, kind in ipairs({ "Hunger", "Thirst" }) do
local rate = satiationRate(mob, kind)
local rate = satiationRate(mob, kind)
local baseRate = satiationRate(baseMob, kind)
local label = kind == "Hunger" and "Голод" or "Жажда"
local label = kind == "Hunger" and "Голод" or "Жажда"
if rate == nil then
if rate == nil then
if baseMob ~= mob and satiationRate(baseMob, kind) then
if baseMob ~= mob and baseRate then
lines[#lines + 1] = "* '''" .. label .. "''': не испытывает."
lines[#lines + 1] = "* '''" .. label .. "''': " .. tone("не испытывает", true) .. "."
end
end
else
else
local cmp = rateCompare(rate, satiationRate(baseMob, kind))
local cmp = rateCompare(rate, baseRate)
if cmp then
if cmp then
lines[#lines + 1] = "* '''" .. label .. "''' нарастает " .. cmp .. "."
lines[#lines + 1] = "* '''" .. label .. "''' нарастает " .. tone(cmp, rate < baseRate) .. "."
end
end
end
end
end
end


-- Естественная регенерация
-- Естественная регенерация: больше — лучше.
local pd = comp("PassiveDamage", mob)
local pd = comp("PassiveDamage", mob)
local heal = pd and healText(pd("damage"))
local bpd = bcomp("PassiveDamage")
local heal = pd and healText(pd("damage"), bpd and bpd("damage"))
if heal then
if heal then
local line = "* '''Естественная регенерация''': " .. heal .. " в секунду"
local line = "* '''Естественная регенерация''': " .. heal .. " в секунду"
local cap = pd("damageCap")
local cap = pd("damageCap")
if cap and cap > 0 then
if cap and cap > 0 then
line = line .. ", пока общий урон не больше " .. num(cap)
line = line .. ", пока общий урон не больше " .. mark(num(cap), cap, bpd and bpd("damageCap"), true)
end
end
lines[#lines + 1] = line .. "."
lines[#lines + 1] = line .. "."
elseif bpd and healText(bpd("damage")) then
lines[#lines + 1] = "* '''Естественная регенерация''': " .. tone("нет", false) .. "."
end
end


Строка 857: Строка 986:
local function environmentLines(mob, orgs)
local function environmentLines(mob, orgs)
local lines = {}
local lines = {}
local baseResp = comp("Respirator", speciesMob(BASELINE_SPECIES))
local baseMob = speciesMob(BASELINE_SPECIES)
local function bcomp(name)
return baseMob and comp(name, baseMob)
end
local baseResp = bcomp("Respirator")


-- Дыхание
-- Дыхание
local resp = comp("Respirator", mob)
local resp = comp("Respirator", mob)
if not resp then
if not resp then
lines[#lines + 1] = "* '''Не дышит''' и не задыхается."
lines[#lines + 1] = "* " .. tone("'''Не дышит''' и не задыхается.", true)
else
else
local types = lungTypes(orgs)
local types = lungTypes(orgs)
Строка 872: Строка 1005:
end
end
-- вредные газы показываем только в сравнении с человеком
-- вредные газы показываем только в сравнении с человеком
local baseTypes = lungTypes(organs(speciesMob(BASELINE_SPECIES)))
local baseTypes = lungTypes(organs(baseMob))
if baseTypes then
if baseTypes then
local _, baseHarm = gases(baseTypes)
local _, baseHarm = gases(baseTypes)
local extra = reagentList(harm, baseHarm)
local extra = reagentList(harm, baseHarm)
if #extra > 0 then
if #extra > 0 then
lines[#lines + 1] = "** В отличие от человека, вредны: " .. table.concat(extra, ", ") .. "."
lines[#lines + 1] = "** В отличие от человека, " .. tone("вредны", false) .. ": " .. table.concat(extra, ", ") .. "."
end
end
local safe = reagentList(baseHarm, harm, breath)
local safe = reagentList(baseHarm, harm, breath)
if #safe > 0 then
if #safe > 0 then
lines[#lines + 1] = "** В отличие от человека, не вредны: " .. table.concat(safe, ", ") .. "."
lines[#lines + 1] = "** В отличие от человека, " .. tone("не вредны", true) .. ": " .. table.concat(safe, ", ") .. "."
end
end
end
end
end
end
local dmg = damageText(resp("damage"))
local dmg = takenText(resp("damage"), baseResp and baseResp("damage"))
local rec = healText(resp("damageRecovery"))
local rec = healText(resp("damageRecovery"), baseResp and baseResp("damageRecovery"))
local interval = seconds(resp("updateInterval"))
local interval = seconds(resp("updateInterval"))
if dmg then
if dmg then
Строка 896: Строка 1029:
local baseSat = baseResp and baseResp("maxSaturation")
local baseSat = baseResp and baseResp("maxSaturation")
if sat and baseSat and baseSat > 0 and math.abs(sat / baseSat - 1) > 0.01 then
if sat and baseSat and baseSat > 0 and math.abs(sat / baseSat - 1) > 0.01 then
lines[#lines + 1] = "** Запас воздуха: ×" .. num(sat / baseSat) .. " от человеческого."
lines[#lines + 1] = "** Запас воздуха: " .. mark("×" .. num(sat / baseSat), sat, baseSat, true) .. " от человеческого."
end
end
end
end


-- Температура (новая схема: TemperatureDamage, Goob: Temperature)
-- Температура (новая схема: TemperatureDamage, Goob: Temperature).
-- Нижний порог: ниже — лучше; верхний: выше — лучше; урон: меньше — лучше.
local t = comp("TemperatureDamage", mob) or comp("Temperature", mob)
local t = comp("TemperatureDamage", mob) or comp("Temperature", mob)
local bt = bcomp("TemperatureDamage") or bcomp("Temperature")
local function bget(field)
return bt and bt(field)
end
if t and t("heatDamageThreshold") then
if t and t("heatDamageThreshold") then
local cold, heat = t("coldDamageThreshold"), t("heatDamageThreshold")
local cold, heat = t("coldDamageThreshold"), t("heatDamageThreshold")
local coldDmg, heatDmg = damageText(t("coldDamage")), damageText(t("heatDamage"))
local coldDmg = takenText(t("coldDamage"), bget("coldDamage"))
local heatDmg = takenText(t("heatDamage"), bget("heatDamage"))
local heatText = mark(kelvinToC(heat), heat, bget("heatDamageThreshold"), true, kelvinToC)
if cold and cold > 0 and coldDmg then
if cold and cold > 0 and coldDmg then
lines[#lines + 1] = "* '''Безопасная температура''': от " .. kelvinToC(cold) .. " до " .. kelvinToC(heat) .. "."
lines[#lines + 1] = "* '''Безопасная температура''': от "
.. mark(kelvinToC(cold), cold, bget("coldDamageThreshold"), false, kelvinToC) .. " до " .. heatText .. "."
lines[#lines + 1] = "** Ниже — " .. coldDmg .. " в секунду."
lines[#lines + 1] = "** Ниже — " .. coldDmg .. " в секунду."
else
else
lines[#lines + 1] = "* '''Безопасная температура''': до " .. kelvinToC(heat) .. ", холода не боится."
lines[#lines + 1] = "* '''Безопасная температура''': до " .. heatText .. ", " .. tone("холода не боится", true) .. "."
end
end
if heatDmg then
if heatDmg then
Строка 916: Строка 1057:
end
end


-- Давление
-- Давление: урон меньше — лучше.
local baro = comp("Barotrauma", mob)
local baro = comp("Barotrauma", mob)
local bbaro = bcomp("Barotrauma")
if not baro or damageSum(baro("damage")) == 0 then
if not baro or damageSum(baro("damage")) == 0 then
lines[#lines + 1] = "* '''Давление''': урона от низкого и высокого давления не получает."
lines[#lines + 1] = "* '''Давление''': " .. tone("урона от низкого и высокого давления не получает", true) .. "."
else
else
lines[#lines + 1] = "* '''Давление''': без защиты в вакууме и при высоком давлении получает в секунду до: "
lines[#lines + 1] = "* '''Давление''': без защиты в вакууме и при высоком давлении получает в секунду до: "
.. damageText(baro("damage")) .. " (зависит от давления)."
.. takenText(baro("damage"), bbaro and bbaro("damage")) .. " (зависит от давления)."
end
end


Строка 928: Строка 1070:
end
end


-- Список «Боевые характеристики».
-- Список «Боевые характеристики». Урон и скорость атаки: больше — лучше.
local function combatLines(mob)
local function combatLines(mob)
local lines = {}
local lines = {}
local baseMob = speciesMob(BASELINE_SPECIES)
local bmw = baseMob and comp("MeleeWeapon", baseMob)
local mw_ = comp("MeleeWeapon", mob)
local mw_ = comp("MeleeWeapon", mob)
-- Типы урона у рас разные (у человека ушибы, у вокса порезы), поэтому сравниваем сумму.
local dmg = mw_ and damageText(mw_("damage"))
local dmg = mw_ and damageText(mw_("damage"))
if dmg then
if dmg then
dmg = mark(dmg, damageSum(mw_("damage")), bmw and damageSum(bmw("damage")), true)
local line = "* '''Безоружная атака''': " .. dmg .. "."
local line = "* '''Безоружная атака''': " .. dmg .. "."
local rate = mw_("attackRate")
local rate = mw_("attackRate")
local baseRate = bmw and bmw("attackRate") or 1
if rate and math.abs(rate - 1) > 0.001 then
if rate and math.abs(rate - 1) > 0.001 then
line = line .. " Скорость атаки: " .. num(rate) .. " удара в секунду."
line = line .. " Скорость атаки: " .. mark(num(rate), rate, baseRate, true) .. " удара в секунду."
end
end
lines[#lines + 1] = line
lines[#lines + 1] = line
Строка 981: Строка 1128:
local h, x, b, n = reagentList(heal), reagentList(harm), reagentList(blocked), reagentList(safe)
local h, x, b, n = reagentList(heal), reagentList(harm), reagentList(blocked), reagentList(safe)
if #h > 0 then
if #h > 0 then
lines[#lines + 1] = "* '''Лечат''': " .. capped(h, 20) .. "."
lines[#lines + 1] = "* " .. tone("'''Лечат'''", true) .. ": " .. capped(h, 20) .. "."
end
end
if #x > 0 then
if #x > 0 then
lines[#lines + 1] = "* '''Вредят''': " .. capped(x, 20) .. "."
lines[#lines + 1] = "* " .. tone("'''Вредят'''", false) .. ": " .. capped(x, 20) .. "."
end
end
if #b > 0 then
if #b > 0 then
lines[#lines + 1] = "* '''Не лечат''' (человека лечат): " .. capped(b, 20) .. "."
lines[#lines + 1] = "* " .. tone("'''Не лечат'''", false) .. " (человека лечат): " .. capped(b, 20) .. "."
end
end
if #n > 0 then
if #n > 0 then
lines[#lines + 1] = "* '''Не вредят''' (человеку вредят): " .. capped(n, 20) .. "."
lines[#lines + 1] = "* " .. tone("'''Не вредят'''", true) .. " (человеку вредят): " .. capped(n, 20) .. "."
end
end
return lines
return lines
Строка 1092: Строка 1239:
{ "NoSlip", function() return "Не поскальзываются." end },
{ "NoSlip", function() return "Не поскальзываются." end },
{ "HardSlip", function(c)
{ "HardSlip", function(c)
local d = damageText(c("fallDamage"))
local d = takenText(c("fallDamage"))
return "Падая при поскальзывании, получают урон" .. (d and (" (" .. d .. ")") or "") .. "."
return "Падая при поскальзывании, получают урон" .. (d and (" (" .. d .. ")") or "") .. "."
end },
end },
Строка 1101: Строка 1248:
end },
end },
{ "DamagedByFlashing", function(c)
{ "DamagedByFlashing", function(c)
local d = damageText(c("flashDamage"))
local d = takenText(c("flashDamage"))
return "Вспышки наносят им урон" .. (d and (" (" .. d .. ")") or "") .. "."
return "Вспышки наносят им урон" .. (d and (" (" .. d .. ")") or "") .. "."
end },
end },
Строка 1157: Строка 1304:
{ "WeaponsUseInability", function() return "Плохо обращаются с крупным оружием." end },
{ "WeaponsUseInability", function() return "Плохо обращаются с крупным оружием." end },
{ "DamageOnHit", function(c)
{ "DamageOnHit", function(c)
local d = damageText(c("damage"))
local d = takenText(c("damage"))
return d and ("Сами получают урон при каждом ударе (" .. d .. ").") or nil
return d and ("Сами получают урон при каждом ударе (" .. d .. ").") or nil
end },
end },
{ "DamageOnLand", function(c)
{ "DamageOnLand", function(c)
local d = damageText(c("damage"))
local d = takenText(c("damage"))
return d and ("Получают урон, когда их бросают (" .. d .. ").") or nil
return d and ("Получают урон, когда их бросают (" .. d .. ").") or nil
end },
end },
Строка 1218: Строка 1365:
return "Огонь не наносит им урона."
return "Огонь не наносит им урона."
end
end
return "Горят " .. (a > b and "сильнее" or "слабее") .. ", чем человек: " .. damageText(f("damage")) .. " в секунду при горении."
return "Горят " .. (a > b and "сильнее" or "слабее") .. ", чем человек: " .. takenText(f("damage"), base("damage")) .. " в секунду при горении."
end
end


Строка 1288: Строка 1435:
}
}
end
end
-- Подзаголовок раздела внутри блока расы. Это не заголовок вики (== ==),
-- а жирная строка: так в оглавление попадают только сами расы.
-- level= (например «===») возвращает обычные заголовки, если они нужны.
local function subHeading(title, level)
if level and level ~= "" then
return level .. " " .. title .. " " .. level
end
return '<div style="font-weight:bold;font-size:1.15em;margin:0.8em 0 0.3em;'
.. 'border-bottom:1px solid #3d3d4a;padding-bottom:2px">' .. title .. '</div>'
end
-- Разделы, которые по умолчанию свёрнуты (раскрываются по клику «показать»).
-- Сейчас пусто: раса и так лежит в шторке, вложенные шторки не нужны.
-- Пример: { ["Внутренние органы"] = true }.
local COLLAPSED = {}


function p.stats(frame)
function p.stats(frame)
local id, args = argId(frame)
local id, args = argId(frame)
isBaseline = (id == BASELINE_SPECIES)
local mob = speciesMob(id)
local mob = speciesMob(id)
if not mob then
if not mob then
return ""
return ""
end
end
local level = args.level or "==="
local orgs = organs(mob)
local orgs = organs(mob)
local out = {}
local out = {}
local function section(title, lines)
local function section(title, lines)
if #lines > 0 then
if #lines == 0 then
out[#out + 1] = level .. " " .. title .. " " .. level
return
end
if COLLAPSED[title] and not args.level then
-- Встроенное сворачивание MediaWiki: заголовок виден всегда, список — по клику.
out[#out + 1] = '<div class="mw-collapsible mw-collapsed" data-expandtext="показать" data-collapsetext="скрыть">'
.. subHeading(title)
out[#out + 1] = '<div class="mw-collapsible-content">'
out[#out + 1] = table.concat(lines, "\n")
out[#out + 1] = '</div></div>'
else
out[#out + 1] = subHeading(title, args.level)
out[#out + 1] = table.concat(lines, "\n")
out[#out + 1] = table.concat(lines, "\n")
end
end
Строка 1313: Строка 1486:
end
end


-- Список особенностей расы по её компонентам (без заголовка).
-- Список особенностей расы по её компонентам.
-- notes= — строки, написанные вручную (идут первыми); header= — заголовок раздела,
-- notes= — строки, написанные вручную (идут первыми); header= — подзаголовок раздела,
-- который выводится, только если есть хоть одна строка.
-- который выводится, только если есть хоть одна строка.
function p.features(frame)
function p.features(frame)
local id, args = argId(frame)
local id, args = argId(frame)
isBaseline = (id == BASELINE_SPECIES)
local mob = speciesMob(id)
local mob = speciesMob(id)
if not mob then
if not mob then
Строка 1336: Строка 1510:
local text = table.concat(lines, "\n")
local text = table.concat(lines, "\n")
if args.header and args.header ~= "" then
if args.header and args.header ~= "" then
local level = args.level or "==="
text = subHeading(args.header, args.level) .. "\n" .. text
text = level .. " " .. args.header .. " " .. level .. "\n" .. text
end
end
return text
return text
end
-- Разбор списка рас для nav и summary: «Human:Люди, Skeleton*:Скелеты, …».
-- ID — id прототипа расы, после двоеточия — заголовок её раздела на странице (на него
-- ведёт ссылка). Звёздочка после ID — показывать, даже если раса недоступна при создании
-- персонажа. Расы, которых нет в проекте, пропускаются.
local function raceList(text)
local out = {}
for part in mw.text.gsplit(text or "", ",", true) do
local idPart, title = part:match("^%s*([^:]+):(.-)%s*$")
if idPart then
local any = idPart:sub(-1) == "*"
local id = mw.text.trim(any and idPart:sub(1, -2) or idPart)
if (any and species(id)) or isPlayable(id) then
out[#out + 1] = { id = id, title = mw.text.trim(title) }
end
end
end
return out
end
-- Сетка иконок рас со ссылками на их разделы:
-- {{#invoke:SpeciesStats|nav|Human:Люди, Dwarf:Дворфы, …}}
function p.nav(frame)
local _, args = argId(frame)
local cards = {}
for _, r in ipairs(raceList(args[1])) do
local mob = speciesMob(r.id)
local file = mob and imageFor(mob)
cards[#cards + 1] = '<div style="width:112px;font-size:0.9em;text-align:center;background:#27272e;'
.. 'border:1px solid #3d3d4a;border-radius:6px;padding:4px">'
.. (file and ("[[Файл:" .. file .. "|64px|link=#" .. r.title .. "]]<br>") or "")
.. "[[#" .. r.title .. "|" .. r.title .. "]]</div>"
end
if #cards == 0 then
return ""
end
return '<div style="display:flex;flex-wrap:wrap;gap:8px;margin:0.5em 0 1em">'
.. table.concat(cards) .. '</div>'
end
-- Сводная таблица рас (сортируется по клику на заголовок столбца):
-- {{#invoke:SpeciesStats|summary|Human:Люди, Dwarf:Дворфы, …}}
-- Цвета — как на всей странице: сравнение с человеком.
function p.summary(frame)
local _, args = argId(frame)
local baseMob = speciesMob(BASELINE_SPECIES)
local function bc(name)
return baseMob and comp(name, baseMob)
end
local baseTh = thresholdsOf(baseMob)
local baseMs = bc("MovementSpeedModifier")
local _, baseVol = bloodOf(bc("Bloodstream"))
local bt = bc("TemperatureDamage") or bc("Temperature")
local rows = {
'{| class="wikitable sortable" style="text-align:center"',
"! Раса !! Крит !! Смерть !! Шаг !! Бег !! Кровь, ед. !! Дышит !! Темп. от !! Темп. до",
}
local function cell(sortValue, text)
if sortValue == nil then
return '| data-sort-value="-9999" | —'
end
return '| data-sort-value="' .. sortValue .. '" | ' .. text
end
for _, r in ipairs(raceList(args[1])) do
local mob = speciesMob(r.id)
if mob then
isBaseline = (r.id == BASELINE_SPECIES)
local th = thresholdsOf(mob)
local ms = comp("MovementSpeedModifier", mob)
local walk, run = ms and ms("baseWalkSpeed"), ms and ms("baseSprintSpeed")
local bsComp = comp("Bloodstream", mob)
local _, vol = bloodOf(bsComp)
if not bsComp then
vol = 0
end
local t = comp("TemperatureDamage", mob) or comp("Temperature", mob)
local cold = t and t("coldDamageThreshold")
local heat = t and t("heatDamageThreshold")
if cold == 0 then
cold = nil
end
local breathText = "не дышит"
if comp("Respirator", mob) then
local types = lungTypes(organs(mob))
local b = types and reagentList((gases(types))) or {}
breathText = #b > 0 and table.concat(b, ", ") or "—"
end
rows[#rows + 1] = "|-"
rows[#rows + 1] = "| style=\"text-align:left\" | [[#" .. r.title .. "|" .. r.title .. "]]"
rows[#rows + 1] = cell(th.Critical, th.Critical and mark(num(th.Critical), th.Critical, baseTh.Critical, true))
rows[#rows + 1] = cell(th.Dead, th.Dead and mark(num(th.Dead), th.Dead, baseTh.Dead, true))
rows[#rows + 1] = cell(walk, walk and mark(num(walk), walk, baseMs and baseMs("baseWalkSpeed"), true))
rows[#rows + 1] = cell(run, run and mark(num(run), run, baseMs and baseMs("baseSprintSpeed"), true))
rows[#rows + 1] = cell(vol, vol and (vol == 0 and "нет" or mark(num(vol), vol, baseVol, true)))
rows[#rows + 1] = "| " .. breathText
rows[#rows + 1] = cell(cold, cold and mark(kelvinToC(cold), cold, bt and bt("coldDamageThreshold"), false, kelvinToC))
rows[#rows + 1] = cell(heat, heat and mark(kelvinToC(heat), heat, bt and bt("heatDamageThreshold"), true, kelvinToC))
end
end
isBaseline = false
rows[#rows + 1] = "|}"
return table.concat(rows, "\n")
end
-- Имя файла картинки расы (спрайт бота) или file=, если задан; пусто, если картинки нет.
-- {{#invoke:SpeciesStats|image|Arachnid|file=Арахнид.png}}
function p.image(frame)
local id, args = argId(frame)
if args.file and args.file ~= "" then
return args.file
end
local mob = speciesMob(id)
return mob and imageFor(mob) or ""
end
end