Module:LocUnitData

From Heroes of Might and Magic: Olden Era Official Wiki
Revision as of 14:35, 15 November 2025 by Krom (talk | contribs)

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 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.growth,
        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

-- Units in tabber 
function p.unitTabs(frame)
    local lang = frame.args.lang or "en"
    local tabs = {}
    
    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 } }
        
        -- local content = p.getUnit{ args = { name = unit, lang = lang } }
 local content = frame:preprocess(
            "{{#invoke:LocUnitData|getUnit|" .. unit .. "|lang=" .. lang .. "}}")
        -- Очищаем и проверяем содержимое
        content = tostring(content):gsub("{%s*$", "") -- Убираем висящие {
        content = content:gsub("}{", "} {") -- Разделяем слипшиеся теги
        
        if content:find("{") and not content:find("}") then
            -- Есть незакрытые фигурные скобки - добавляем закрывающую
            content = content .. "}"
        end
        
        -- добавляем таб в таблицу
        table.insert(tabs, {
            label = title,
            content = content
        })
    end
    
    if #tabs == 0 then return "" end
    
    return mw.ext.tabber.render(tabs)
end
return p