Module:LocUnitData: Difference between revisions
From Heroes of Might and Magic: Olden Era Official Wiki
No edit summary |
No edit summary Tag: Manual revert |
||
| Line 15: | Line 15: | ||
"Swordsman", | "Swordsman", | ||
"Guard Captain", | "Guard Captain", | ||
" | "Sun's Aegis", | ||
"Crossbowman", | "Crossbowman", | ||
"Austringer", | "Austringer", | ||
| Line 91: | Line 91: | ||
"Binder", | "Binder", | ||
"Votary", | "Votary", | ||
" | "Aga'Shoth Rider", | ||
" | "Aga'Shoth Tamer", | ||
" | "Aga'Shoth Matha", | ||
"Grand Shoth", | "Grand Shoth", | ||
"Unspeakable Shoth", | "Unspeakable Shoth", | ||
Revision as of 12:55, 19 November 2025
Documentation for this module may be created at Module:LocUnitData/doc
local p = {}
-- Подключаем зависимости
local LangStrings = require("Module:LocGetString")
local LocGameEntities = require("Module:LocGameEntities")
local dataCache = nil
-----------------------------------------------
-- ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ
-----------------------------------------------
local factions = {
Temple = {
"Swordsman",
"Guard Captain",
"Sun's Aegis",
"Crossbowman",
"Austringer",
"Marksman",
"Griffin",
"Temple Griffin",
"Guardian Griffin",
"Lightweaver",
"Hierophant",
"Sun Herald",
"Cavalry",
"Noble Cavalry",
"Sunspear Cavalry",
"Inquisitor",
"Mother Superior",
"Excommunicator",
"Angel",
"Archangel",
"Apotheosis"
},
Necropolis = {
"Skeleton",
"Skeleton Warrior",
"Skeleton Archer",
"Wight",
"Wraith",
"Phantasm",
"Undead Pet",
"Barghest",
"Armoured Hound",
"Graverobber",
"Merchant of Death",
"Kennelmaster",
"Lich",
"Pestilent Lich",
"Sanguine Lich",
"Dread Knight",
"Avatar of War",
"Hollow Reaper",
"Vampire",
"Vampire Lord",
"Vampire Scholar"
},
Dungeon = {
"Troglodyte",
"Infernal Troglodyte",
"Toxic Troglodyte",
"Infiltrator",
"Guile Infiltrator",
"Bleak Infiltrator",
"Onyx Dancer",
"Jasper Dancer",
"Aureate Dancer",
"Minotaur",
"Minotaur Lord",
"Minotaur Vanguard",
"Medusa",
"Medusa Sculptor",
"Medusa Queen",
"Hydra",
"Cthonic Hydra",
"Infernal Hydra",
"Cave Dragon",
"Black Dragon",
"Ashen Dragon"
},
Schism = {
"Ra'Shoth",
"Stinging Ra'Shoth",
"Ferocious Ra'Shoth",
"Cultist",
"Binder",
"Votary",
"Aga'Shoth Rider",
"Aga'Shoth Tamer",
"Aga'Shoth Matha",
"Grand Shoth",
"Unspeakable Shoth",
"Unthinkable Shoth",
"Concubus",
"Mistress of Chains",
"Bewitcher",
"Arbitrator",
"Rift Arbitrator",
"Bloated Arbitrator",
"Abyssal Envoy",
"Abyssal Overseer",
"Abyssal Executor"
},
Sylvan = {
"Faun",
"Faun Archer",
"Faun Warrior",
"Hoplet",
"Dawn Hoplet",
"Dusk Hoplet",
"Vine Iriyad",
"Fungal Iriyad",
"Crystal Iriyad",
"Aqualotl",
"Blooming Aqualotl",
"Polar Aqualotl",
"Herbomancer",
"Sporemancer",
"Murmurmancer",
"Qilin",
"Thunder Qilin",
"Mist Qilin",
"Phoenix",
"Flaming Phoenix",
"Energy Phoenix"
},
Hive = {
"Gnat",
"Warden Gnat",
"Ravager Gnat",
"Locust",
"Overgrown Locust",
"Harvester Locust",
"Hornet",
"Chanter",
"Stinger",
"Mantis",
"Vermilion Mantis",
"Royal Mantis",
"Reaver",
"Menacing Reaver",
"Maniacal Reaver",
"Waurms",
"Devourer",
"Pyroboros",
"Hive Queen",
"Hive Mother",
"Hive Huntress"
},
Neutral = {
"Pixie",
"Peasant",
"Ghoul",
"Halfling",
"Animated Armour",
"Groll",
"Kittenhorn",
"Starchild",
"Primal Remnant",
"Unicorn",
"Giant Toad",
"Faerie Dragon",
"Couatl",
"Dragonslayer",
"Worldwatcher",
"Sentinel of Glory",
"Obsidian Dragon",
"Lich Dragon",
"Avatar"
}
}
-- Таблица с иконками фракций
local factionIcons = {
Temple = "Human icon.png",
Necropolis = "Undead icon.png",
Dungeon = "Dungeon icon.png",
Schism = "Unfrozen icon.png",
Sylvan = "Spring icon.png",
Hive = "Hive icon.png",
Neutral = "Primordial_Chaos.png"
}
-- Таблица с названиями фракций
local factionNames = {
Temple = "Temple",
Necropolis = "Necropolis",
Dungeon = "Dungeon",
Schism = "Schism",
Sylvan = "Sylvan",
Hive = "Hive",
Neutral = "Neutral"
}
-- Таблица с путями для фракций
local factionPages = {
Temple = "Temple_Units",
Necropolis = "Necropolis_Units",
Dungeon = "Dungeon_Units",
Schism = "Schism_Units",
Sylvan = "Sylvan_Units",
Hive = "Hive_Units",
Neutral = "Neutral_Units"
}
-----------------------------------------------------
-- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
-----------------------------------------------------
local function loadJson(title)
local page = mw.title.new(title)
local content = page and page:getContent() or ""
if content == "" then return {} end
local ok, data = pcall(mw.text.jsonDecode, content)
if not ok then return {} end
return data
end
local function loadUnits()
if dataCache then return dataCache end
dataCache = {}
local stats = loadJson("Data:UnitsStats.json")
local data = loadJson("Data:UnitsData.json")
-- Индексируем stats по id
local statsById = {}
for key, item in pairs(stats) do
local id = item.id or key
statsById[id] = item
end
-- Объединяем
for name, item in pairs(data) do
if item.id and statsById[item.id] then
local merged = {}
for k, v in pairs(statsById[item.id]) do merged[k] = v end
for k, v in pairs(item) do merged[k] = v end
dataCache[name] = merged
end
end
return dataCache
end
local function getLabel(sid, lang)
local text = LangStrings.getText({ args = { sid = sid, lang = lang } })
return text or "—"
end
local function getUnitLabels(lang)
return {
Attack = getLabel("unit_attack", lang),
Defense = getLabel("unit_defence", lang),
Damage = getLabel("unit_damage", lang),
Health = getLabel("unit_health", lang),
Speed = getLabel("unit_speed", lang),
Initiative = getLabel("unit_init", lang),
Morale = getLabel("unit_moral", lang),
Luck = getLabel("unit_luck", lang),
Tier = getLabel("tooltipRang", lang),
Fraction = getLabel("faction_laws", lang),
Price = "[[File:Skill Economy.png|24px|alt=Price]]",
Experience = getLabel("hero_ui_exp", lang),
SquadValue = "SquadValue",
Range = "[[File:Battle icon Onesword.png|24px|alt=Attack Type]]",
Growth = "[[File:Skill Recruitment.png|24px|alt=Growth]]",
Movement = "[[File:Icon Stats Moving.png|24px|alt=Attack Type]]"
}
end
local function getLocFraction(code, lang)
local valid = {
human = true, demon = true, dungeon = true,
nature = true, undead = true, unfrozen = true
}
if not valid[code] then return "-" end
local sid = "world_cheat_dropdown_fraction_" .. code
return LangStrings.getText({ args = { sid = sid, lang = lang } })
end
function formatCost(cost)
if not cost then return "" end
local resourceOrder = {"gold", "gemstones", "mercury", "crystals"}
local resources = {
gold = {icon = "Icon Resource Gold.png", link = "Resources#Gold", name = "Gold"},
gemstones = {icon = "Icon Resource GemStone.png", link = "Resources#Gems", name = "Gems"},
mercury = {icon = "Icon Resource Mercury.png", link = "Resources#Mercury", name = "Mercury"},
crystals = {icon = "Icon Resource Crystal.png", link = "Resources#crystals", name = "Crystals"}
}
local parts = {}
for _, resource in ipairs(resourceOrder) do
if cost[resource] then
table.insert(parts, cost[resource] .. " [[File:" .. resources[resource].icon .. "|24px|link=" .. resources[resource].link .. "|" .. resources[resource].name .. "]]")
end
end
return table.concat(parts, " ")
end
function getUnitNamebyId(id)
local units = loadUnits()
for name, unit in pairs(units) do
if unit.id == id then
return name
end
end
return nil
end
-- Возвращает:
-- baseUnitName, mainUpgName, altUpgName
function resolveUpgrades(unit)
if not unit or not unit.id then
return nil, nil, nil
end
local id = unit.id
-- определяем корневой id
local rootId = id
:gsub("_upg_alt$", "")
:gsub("_upg$", "")
-- id апгрейдов
local mainUpgId = rootId .. "_upg"
local altUpgId = rootId .. "_upg_alt"
-- имена
local baseUnitName = getUnitNamebyId(rootId)
local mainUpgName = getUnitNamebyId(mainUpgId)
local altUpgName = getUnitNamebyId(altUpgId)
return baseUnitName, mainUpgName, altUpgName
end
-----------------------------------------------------
-- ОСНОВНАЯ ЛОГИКА: получение данных юнита
-----------------------------------------------------
function p.getUnitData(name, lang)
if not name then return nil, "No unit name provided" end
lang = lang or "en"
local units = loadUnits()
local unit = units[name]
if not unit then return nil, "Unit not found: " .. name end
local stats = unit.stats or {}
local cost = formatCost(unit.unitCost) or ""
-- локализованные данные
local locName = LocGameEntities.getLocName({ args = { name = name, lang = lang } })
local locDesc = LocGameEntities.getLocDescription({ args = { name = name, lang = lang } })
local image = LocGameEntities.getImage({ args = { name = name, lang = lang } })
local icon = LocGameEntities.getIcon({ args = { name = name, lang = lang } })
local fraction = getLocFraction(unit.fraction, lang)
-- способности
local abilities = {}
if stats.moveType then
local moveType = "movetype_" .. stats.moveType
table.insert(unit.abilities, 1, moveType)
unit.moveType = LocGameEntities.getLocName({ args = { name = moveType, lang = lang } })
end
for _, aid in ipairs(unit.abilities or {}) do
local aname = LocGameEntities.getLocName({ args = { name = aid, lang = lang } })
local adesc = LocGameEntities.getLocDescription({ args = { name = aid, lang = lang } })
local aicon = LocGameEntities.getIcon({ args = { name = aid, lang = lang } })
if aname and aname ~= "" then
table.insert(abilities, { id = aid, name = aname, description = adesc, icon = aicon })
if aid == "attack_melee" or aid == "attack_range" or aid == "attack_shoot" then
unit.attack_type = aname
end
end
end
local baseUnit, upgUnit, upgAltUnit = resolveUpgrades(unit)
return {
id = unit.id,
upgradeLine = { baseUnit, upgUnit, upgAltUnit },
name = name,
locName = locName,
locDesc = locDesc,
image = image,
icon = icon,
tier = unit.tier,
fraction = fraction,
attack_type = unit.attack_type,
stats = stats,
cost = cost,
growth = unit.weeklyIncrement,
moveType = unit.moveType,
expBonus = unit.expBonus,
squadValue = unit.squadValue,
abilities = abilities
}
end
-----------------------------------------------------
-- РИСОВАНИЕ ТАБЛИЦЫ
-----------------------------------------------------
function p.renderUnitTable(unit, lang, baseUnit)
if not unit then return "Invalid unit data" end
lang = lang or "en"
local L = getUnitLabels(lang)
local function val(x)
if x == nil or x == "" then return "—" end
return tostring(x)
end
local function diffStat(stat)
if not baseUnit then return val(unit.stats[stat]) end
local newVal = tonumber(unit.stats[stat])
local oldVal = tonumber(baseUnit.stats[stat])
if not newVal or not oldVal then return val(unit.stats[stat]) end
if newVal > oldVal then
return string.format("%s <span style='color:green;font-weight:bold;'>↑</span>", val(newVal))
elseif newVal < oldVal then
return string.format("%s <span style='color:red;font-weight:bold;'>↓</span>", val(newVal))
else
return val(newVal)
end
end
local stats = unit.stats or {}
local cost = unit.cost or {}
text = "<div style='max-width:1000px;margin:0 auto;'>\n"
-- заголовок юнита
text = text .. "<div style='text-align:left;line-height:1.4;margin-bottom:8px;'>"
text = text .. "<div style='font-size:1.5em;font-weight:bold;'>" .. unit.locName .. "</div>"
text = text .. "<div style='font-size:0.9em;color:gray;'>" .. val(unit.locDesc) .. "</div>"
text = text .. "</div>\n"
-- основной блок: таблица статов + картинка
text = text .. "<div style='display:flex;flex-wrap:wrap;justify-content:center;gap:10px;'>\n"
-- левая часть: таблица статов
text = text .. "<div style='flex:1 1 400px;min-width:280px;'>\n"
text = text .. "{| class='wikitable' style='width:100%;text-align:left;margin:0;'\n"
text = text .. "|-\n|" .. "[[File:Health Icon.png|24px|link=Combat#Health|alt=Health]] ".. L.Health .. " || " .. diffStat("hp") .. " || " .. L.Tier .. " || " .. val(unit.tier) .. "\n"
text = text .. "|-\n|" .. "[[File:Icon Stats Attack.png|24px|link=Combat#Attack|alt=Attack]] ".. L.Attack .. " || " .. diffStat("offence") .. " || " .. L.Fraction .. " || " .. val(unit.fraction) .. "\n"
text = text .. "|-\n|" .. "[[File:Icon Stats Defence.png|24px|link=Combat#Defence|alt=Defense]] ".. L.Defense .. " || " .. diffStat("defence") .. " || " .. L.Range .. " || " .. val(unit.attack_type) .. "\n"
text = text .. "|-\n|" .. "[[File:Icon Stats Damage.png|24px|link=Combat#Damage|alt=Damage]] ".. L.Damage .. " || " .. diffStat("damageMin") .. "–" .. diffStat("damageMax") .. " || " .. L.Movement .. " || " .. val(unit.moveType) .. "\n"
text = text .. "|-\n|" .. "[[File:Icon Stats Speed.png|24px|link=Combat#Speed|alt=Speed]] ".. L.Speed .. " || " .. diffStat("speed") .. " || " .. L.Price .. " || " .. cost .. "\n"
text = text .. "|-\n|" .. "[[File:Icon Stats Initiative.png|24px|link=Combat#Initiative|alt=Initiative]] ".. L.Initiative .. " || " .. diffStat("initiative") .. " || " .. L.Growth .. " || " .. val(unit.growth) .. "\n"
text = text .. "|-\n|" .. "[[File:Icon Stats Morale.png|24px|link=Combat#Morale|alt=Morale]] ".. L.Morale .. " || " .. diffStat("moral") .. " || " .. L.Experience .. " || " .. val(unit.expBonus) .. "\n"
text = text .. "|-\n|" .. "[[File:Icon Stats Luck.png|24px|link=Combat#Luck|alt=Luck]] ".. L.Luck .. " || " .. diffStat("luck") .. " || " .. L.SquadValue .. " || " .. val(unit.squadValue) .. "\n"
text = text .. "|}\n"
text = text .. "</div>\n"
-- правая часть: картинка
text = text .. "<div style='flex:1 1 300px;min-width:260px;text-align:center;'>[[File:" .. (unit.image or "Temple_Swordsman_Battlefield.png") .. "|400px]]</div>\n"
text = text .. "</div>\n" -- закрытие блока flex
-- способности
text = text .. "<div style='margin-top:10px;'>\n"
if unit.abilities and #unit.abilities > 0 then
text = text .. "{| class='wikitable' style='width:100%;text-align:left;margin:0;'\n"
for _, ab in ipairs(unit.abilities) do
local icon = "[[File:" .. (ab.icon ~= "" and ab.icon or "icon bug.png") .. "|60px]]"
text = text .. "|-\n| style='text-align:center;width:80px;vertical-align:middle;' | " .. icon ..
" || style='padding:10px;' | <b>" .. ab.name .. "</b> — " .. ab.description .. "\n"
end
text = text .. "|}\n"
else
text = text .. "—\n"
end
text = text .. "</div>\n"
text = text .. "</div>\n"
return text
end
-----------------------------------------------------
-- Главная точка входа для шаблона
-----------------------------------------------------
function p.getUnit(frame)
local name = frame.args[1] or frame.args.name
local lang = frame.args.lang or "en"
local unit, err = p.getUnitData(name, lang)
if not unit then return err end
return p.renderUnitTable(unit, lang)
end
function p.getUnits(frame)
local name = frame.args[1] or frame.args.name
local lang = frame.args.lang or "en"
local unit, err = p.getUnitData(name, lang)
if not unit then
return err
end
-- основной юнит
local text = p.renderUnitTable(unit, lang)
-- связанные из линейки
for _, unitName in ipairs(unit.upgradeLine) do
if unitName and unitName ~= name then
local relatedUnit = p.getUnitData(unitName, lang)
text = text .. "\n\n" .. p.renderUnitTable(relatedUnit, lang, unit)
end
end
return text
end
-- TABS for units
function p.unitTabs(frame)
local lang = frame.args.lang or "en"
local tabberContent = {}
for i = 1, math.huge do
local unit = frame.args[i]
if not unit then break end
unit = mw.text.trim(unit)
local title = LangStrings.translate{ args = { text = unit, lang = lang } }
table.insert(tabberContent, "|-| " .. title .. " =")
table.insert(tabberContent, "<br>")
table.insert(tabberContent, "{{#invoke:LocUnitData|getUnits|" .. unit .. "|lang=" .. lang .. "}}")
end
if #tabberContent == 0 then return "" end
local fullTabber = "<tabber>\n" .. table.concat(tabberContent, "\n") .. "\n</tabber>"
-- Обрабатываем всю разметку tabber как викитекст
return frame:preprocess(fullTabber)
end
-- NavBox for fractions
function p.navBox(frame)
local faction = frame.args.faction
local lang = frame.args.lang or "en"
local result = '{| class="wikitable mw-collapsible white-toggle-button" style="width:100%;"\n'
result = result .. '!colspan="8"|[[Units' .. (lang ~= "en" and "/" .. lang or "") .. '|Units]]\n'
-- Функция для рендеринга одной фракции
local function renderFaction(fac)
local row = '|-\n'
row = row .. '|align="center"|[[File:' .. factionIcons[fac] .. '|50px|link=' .. factionPages[fac] .. (lang ~= "en" and "/" .. lang or "") .. '|' .. factionNames[fac] .. ' Units]]<br>[[' .. factionPages[fac] .. (lang ~= "en" and "/" .. lang or "") .. '|' .. factionNames[fac] .. ']]\n'
local units = factions[fac]
if fac == "Neutral" then
-- Особый формат для нейтральных юнитов
row = row .. '|colspan="7"|'
for i, unit in ipairs(units) do
row = row .. '[[File:' .. unit:gsub(" ", "_") .. '_icon.png|28px|link=' .. unit .. (lang ~= "en" and "/" .. lang or "") .. ']] [[' .. unit .. (lang ~= "en" and "/" .. lang or "") .. '|' .. unit .. ']]'
if i < #units then
row = row .. ' • '
end
end
else
-- Обычные фракции (7 колонок по 3 юнита)
row = row .. '| '
for i = 1, 21, 3 do
if i > 1 then
row = row .. '\n| '
end
for j = 0, 2 do
if units[i + j] then
local locName = LocGameEntities.getLocName({ args = { name = units[i + j], lang = lang } })
row = row .. '[[File:' .. units[i + j]:gsub(" ", "_") .. '_icon.png|28px|link=' .. units[i + j] .. (lang ~= "en" and "/" .. lang or "") .. ']] [[' .. units[i + j] .. (lang ~= "en" and "/" .. lang or "") .. '|' .. locName .. ']]'
if j < 2 and units[i + j + 1] then
row = row .. ' <br> '
end
end
end
end
end
row = row .. '\n' -- ДОБАВЛЕНО: перенос строки в конце каждой фракции
return row
end
if faction and factions[faction] then
-- Показываем только одну фракцию
result = result .. renderFaction(faction)
else
-- Показываем все фракции
local allFactions = {"Temple", "Necropolis", "Dungeon", "Schism", "Sylvan", "Hive", "Neutral"}
for _, fac in ipairs(allFactions) do
result = result .. renderFaction(fac)
end
end
result = result .. '|}\n'
return result
end
return p


