Module:LocUnitData

From Heroes of Might and Magic: Olden Era Official Wiki
Revision as of 13:14, 19 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 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",
        "Chthonic 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
                local locName = LocGameEntities.getLocName({ args = { name = units[i + j], lang = lang } })
                row = row .. '[[File:' .. unit:gsub(" ", "_") .. '_icon.png|28px|link=' .. unit .. (lang ~= "en" and "/" .. lang or "") .. ']] [[' .. unit .. (lang ~= "en" and "/" .. lang or "") .. '|' .. locName .. ']]'
                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