Модуль:SpeciesStats: различия между версиями
Halch (обсуждение | вклад) Нет описания правки |
Halch (обсуждение | вклад) Нет описания правки |
||
| (не показаны 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])) .. " " .. | 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) .. " " .. | 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 — у человека его нет. | ||
local function healText(spec, base) | |||
local function negOnly(sp) | |||
local neg = { types = {}, groups = {} } | |||
local any = false | |||
if type(sp) == "table" then | |||
for t, v in pairs(sp.types or {}) do | |||
if v < 0 then | |||
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 | ||
local neg, any = negOnly(spec) | |||
if not any then | |||
return nil | |||
end | end | ||
return | 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] = " | -- Плоское снижение: положительное — урона меньше (зелёный), отрицательное — больше (красный). | ||
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 | local base = thresholdsOf(baseMob) | ||
local th = | 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] = "подкрит — с " .. | parts[#parts + 1] = "подкрит — с " .. v .. " урона" | ||
elseif state == "Critical" then | elseif state == "Critical" then | ||
parts[#parts + 1] = "критическое состояние — с " .. | parts[#parts + 1] = "критическое состояние — с " .. v .. " урона" | ||
hasCrit = true | hasCrit = true | ||
elseif state == "Dead" then | elseif state == "Dead" then | ||
parts[#parts + 1] = "смерть — с " .. | 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( | local baseMs = bcomp("MovementSpeedModifier") | ||
.. ", бег " .. num( | local walk, run = ms("baseWalkSpeed") or 0, ms("baseSprintSpeed") or 0 | ||
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 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 = | 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 | ||
-- Голод и жажда | -- Голод и жажда: медленнее — лучше. | ||
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 | if baseMob ~= mob and baseRate then | ||
lines[#lines + 1] = "* '''" .. label .. "''': не испытывает." | lines[#lines + 1] = "* '''" .. label .. "''': " .. tone("не испытывает", true) .. "." | ||
end | end | ||
else | else | ||
local cmp = rateCompare(rate, | 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 = | 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( | 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 = | 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 | 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) .. " до " .. | 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] = "* '''Безопасная температура''': до " .. | 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] = "* '''Давление''': без защиты в вакууме и при высоком давлении получает в секунду до: " | ||
.. | .. 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 = | 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 = | 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 = | 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 = | 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 "слабее") .. ", чем человек: " .. | 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 orgs = organs(mob) | local orgs = organs(mob) | ||
local out = {} | local out = {} | ||
local function section(title, lines) | local function section(title, lines) | ||
if #lines | if #lines == 0 then | ||
out[#out + 1] = | 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 | ||
text = subHeading(args.header, args.level) .. "\n" .. text | |||
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 | ||