Module:LocUnitData: Difference between revisions

From Heroes of Might and Magic: Olden Era Official Wiki
mNo edit summary
m Correct typo in unit name (the game uses american 'Armored' rather than british 'Armoured')
 
(116 intermediate revisions by 2 users not shown)
Line 1: Line 1:
local p = {}
local p = {}


-- Подключаем модуль локализации
-- Подключаем зависимости
local LangStrings = require("Module:LocGetString")
local LangStrings = require("Module:LocGetString")
local LocGameEntities = require("Module:LocGameEntities")
local UnitModule  = require("Module:Unit") -- наш новый модуль


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",
        "Armored 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_(unit)",
        "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 function loadUnits()
local factionIcons = {
     if dataCache then return dataCache end
    Temple = "Human icon.png",
     dataCache = {}
    Necropolis = "Undead icon.png",
    Dungeon = "Dungeon icon.png",
    Schism = "Unfrozen icon.png",
    Sylvan = "Spring icon.png",
     Hive = "Hive icon.png",
     Neutral = "Neutral_faction.png"
}


    local function loadJson(title)
-- Таблица с названиями фракций
        local page = mw.title.new(title)
local factionNames = {
        local content = page and page:getContent() or ""
    Temple = "Temple",
        if content == "" then return {} end
    Necropolis = "Necropolis",
        local ok, data = pcall(mw.text.jsonDecode, content)
    Dungeon = "Dungeon",
        if not ok then return {} end
    Schism = "Schism",
        return data
    Sylvan = "Sylvan",
    end
    Hive = "Hive",
    Neutral = "Neutral"
}


     local stats = loadJson("Data:UnitsStats.json")
-- Таблица с путями для фракций
     local data = loadJson("Data:UnitsData.json")
local factionPages = {
 
     Temple = "Temple_Units",
     -- Индексируем stats по id
     Necropolis = "Necropolis_Units",
     local statsById = {}
     Dungeon = "Dungeon_Units",
     for key, item in pairs(stats) do
     Schism = "Schism_Units",
        local id = item.id or key
     Sylvan = "Sylvan_Units",
        statsById[id] = item
    Hive = "Hive_Units",
    end
    Neutral = "Neutral_Units"
}


    -- Основная таблица по имени
-----------------------------------------------------
    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
-- Обёртка: вызвать Module:Unit get с нужными аргументами
local function unitGet(name, property, lang)
     return UnitModule.get({ args = { name, property, lang or "en" } })
end
end


-- Функции локализации данных
local function getLabel(sid, lang)
local function getLabel(sid, lang)
     local text = LangStrings.getText({ args = { sid = sid, lang = lang } })
     local text = LangStrings.getText({ args = { sid = sid, lang = lang } })
Line 51: Line 224:


local function getUnitLabels(lang)
local function getUnitLabels(lang)
     local L = {}
     return {
    L.Attack      = getLabel("unit_attack", lang)
        Attack      = getLabel("unit_attack", lang),
    L.Defense    = getLabel("unit_defence", lang)
        Defense    = getLabel("unit_defence", lang),
    L.Damage      = getLabel("unit_damage", lang)
        Damage      = getLabel("unit_damage", lang),
    L.Health      = getLabel("unit_health", lang)
        Health      = getLabel("unit_health", lang),
    L.Speed      = getLabel("unit_speed", lang)
        Speed      = getLabel("unit_speed", lang),
    L.Initiative  = getLabel("unit_init", lang)
        Initiative  = getLabel("unit_init", lang),
    L.Morale      = getLabel("unit_moral", lang)
        Morale      = getLabel("unit_moral", lang),
    L.Luck        = getLabel("unit_luck", lang)
        Luck        = getLabel("unit_luck", lang),
    L.Tier        = getLabel("tooltipRang", lang)
        Tier        = getLabel("tooltipRang", lang),
    L.Fraction    = getLabel("faction_laws", lang)
        Fraction    = getLabel("faction_laws", lang),
    L.Price      = 'Price'          -- статичная строка
        Price      = "[[File:Skill Economy.png|24px|alt=Price]]",
    L.Experience  = getLabel("hero_ui_exp", lang)
        Experience  = getLabel("hero_ui_exp", lang),
    L.SquadValue  = 'SquadValue'    -- статичная
        SquadValue  = "SquadValue",
    L.Range      = 'Range'          -- статичная
        Range      = "[[File:Battle icon Onesword.png|24px|alt=Attack Type]]",
    L.Growth      = 'Growth'         -- статичная
        Growth      = "[[File:Skill Recruitment.png|24px|alt=Growth]]",
    L.Movement    = 'Movement'      -- статичная
         Movement    = "[[File:Icon Stats Moving.png|24px|alt=Attack Type]]",
        AbilityRank = getLabel("tooltipRang", lang),
        EnergyCost  = getLabel("unit_energy", lang),
    }
end
 
 


     return L
 
function p.printUnitData(frame)
    local name = frame.args[1] or frame.args.name
    if name then
        name = name:gsub("'", "’")
        name = name:gsub("%_%(unit%)", "")
    else
        return "Ошибка: не указано имя юнита"
    end
   
    local lang = frame.args.lang or "en"
   
    local success, result = pcall(function()
        return p.getUnitData(name, lang)
    end)
   
    if not success then
        return "Ошибка: " .. tostring(result)
    end
   
    -- Простой вывод всей таблицы
    local output = "=== ДАННЫЕ ДЛЯ ОТЛАДКИ ===\n"
    output = output .. "Тип результата: " .. type(result) .. "\n\n"
   
    if type(result) == "table" then
        output = output .. "Содержимое таблицы:\n"
        for key, value in pairs(result) do
            output = output .. "  " .. tostring(key) .. " = " .. tostring(value) .. "\n"
        end
    else
        output = output .. "Результат: " .. tostring(result)
    end
   
     return "<pre>" .. output .. "</pre>"
end
end


local function getLocFraction(code, lang)
-----------------------------------------------------
      
-- ОСНОВНАЯ ЛОГИКА: получение данных юнита
     local valid = {
-----------------------------------------------------
         human = true,
function p.getUnitData(name, lang)
         demon = true,
     if not name then return nil, "No unit name provided" end
        dungeon = true,
    lang = lang or "en"
        nature = true,
 
         undead = true,
    -- Проверяем что юнит существует
         unfrozen = true
     local idCheck = unitGet(name, "id", lang)
    if not idCheck or idCheck == "" or idCheck:find("^Unit '") then
         return nil, "Unit not found: " .. name
    end
 
    -- Базовые поля через Module:Unit
    local locName    = unitGet(name, "name",            lang)
    local locDesc    = unitGet(name, "description",    lang)
    local fraction  = unitGet(name, "faction",         lang)
    local tier      = unitGet(name, "tier",            lang)  -- уже римские цифры
    local cost      = unitGet(name, "cost",            lang)
    local growth    = unitGet(name, "weeklyIncrement", lang)
    local expBonus  = unitGet(name, "expBonus",        lang)
    local squadValue = unitGet(name, "squadValue",      lang)
    local imageName  = unitGet(name, "imageName",      "en")  -- без пробелов/апострофов
 
    -- Линейка апгрейдов (имена-ключи на en, для обхода в getUnits)
    local baseUnitName = unitGet(name, "baseName",      "en")
    local upgUnitName  = unitGet(name, "upgradeName",    "en")
    local altUnitName  = unitGet(name, "altUpgradeName", "en")
 
    -- Статы (числовые поля)
    local statFields = {
         "hp", "offence", "defence", "damageMin", "damageMax",
         "speed", "initiative", "moral", "luck"
     }
     }
    local stats = {}
    for _, f in ipairs(statFields) do
        stats[f] = unitGet(name, f, lang)
    end
    -- moveType и тип атаки
    local moveType  = unitGet(name, "moveType", lang)
    local attack_type = ""


     if not valid[code] then
    -- Способности
         return "-"
    local abilities = {}
 
    -- BaseClass — добавляем первым
    local bcName = unitGet(name, "baseClass.name", lang)
     if bcName and bcName ~= "" then
         local bcDesc = unitGet(name, "baseClass.description", lang)
        local bcIcon = unitGet(name, "baseClass.icon", lang)
        table.insert(abilities, { name = bcName, description = bcDesc, icon = bcIcon, isBaseClass = true })
     end
     end


     local sid = "world_cheat_dropdown_fraction_" .. code
    -- Активные способности
    return LangStrings.getText({ args = { sid = sid, lang = lang } })
     local i = 1
end
    while true do
        local aname = unitGet(name, "abilities." .. i .. ".name", lang)
        if not aname or aname == "" then break end
        local adesc        = unitGet(name, "abilities." .. i .. ".description",        lang)
        local aicon        = unitGet(name, "abilities." .. i .. ".icon",              lang)
        local atype        = unitGet(name, "abilities." .. i .. ".type",              lang)
        local ability_tier = unitGet(name, "abilities." .. i .. ".ability_tier",      lang)
        local energyLevel  = unitGet(name, "abilities." .. i .. ".energyLevel",        lang)
        local excaption    = unitGet(name, "abilities." .. i .. ".excaptionInTooltip", lang)
        table.insert(abilities, {
            name        = aname,
            description  = adesc,
            icon        = aicon,
            atype        = atype,
            ability_tier = ability_tier,
            energyLevel  = energyLevel,
            excaption    = excaption,
        })
        if atype == "attack_melee" or atype == "attack_range" or atype == "attack_shoot" then
            attack_type = aname
        end
        i = i + 1
    end


-- Главная функция для вывода таблицы юнита
    -- Пассивки (добавляем к способностям)
function p.getUnit(frame)
     i = 1
     local name = frame.args[1] or frame.args.name
    while true do
    local lang = frame.args.lang or "en"
        local aname = unitGet(name, "passives." .. i .. ".name", lang)
    if not name then return "No unit name provided" end
        if not aname or aname == "" then break end
        local adesc = unitGet(name, "passives." .. i .. ".description", lang)
        local aicon = unitGet(name, "passives." .. i .. ".icon", lang)
        table.insert(abilities, { name = aname, description = adesc, icon = aicon })
        i = i + 1
    end


     local units = loadUnits()
    -- Изображение: Faction_ImageName_Battlefield.png
     local unit = units[name]
     local factionRaw = unitGet(name, "faction", "en")  -- внутренний ключ (human, nature, ...)
     if not unit then return "Unit not found: " .. name end
     local factionCap = factionRaw ~= "" and (factionRaw:sub(1,1):upper() .. factionRaw:sub(2)) or ""
     local image = factionCap .. "_" .. imageName .. "_Battlefield.png"


     -- Локализованные имя и описание
     return {
    local locName = LocGameEntities.getLocName({ args = { name = name, lang = lang } })
        id          = idCheck,
     local locDesc = LocGameEntities.getLocDescription({ args = { name = name, lang = lang } })
        upgradeLine = { baseUnitName, upgUnitName, altUnitName },
        name       = name,
        locName    = locName,
        locDesc     = locDesc,
        image      = image,
        tier        = tier,
        fraction    = fraction,
        attack_type = attack_type,
        stats      = stats,
        cost        = cost,
        growth      = growth,
        moveType    = moveType,
        expBonus    = expBonus,
        squadValue  = squadValue,
        abilities  = abilities,
    }
end


     local stats = unit.stats or {}
-----------------------------------------------------
     local cost = unit.unitCost or {}
-- РИСОВАНИЕ ТАБЛИЦЫ
-----------------------------------------------------
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)
     local function val(x)
         if x == nil or x == "" then return "—" end
         if x == nil or x == "" then return "—" end
         return x
         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
     end


     -- Локализованные подписи
    local stats = unit.stats or {}
     local L = getUnitLabels(lang)
    local cost = unit.cost or {}
 
 
     -- Формирование таблицы без иконок
    text = "<div style='max-width:1000px;margin:0 auto;'>\n"
     local text = "{| class='wikitable' style='max-width:1000px;margin-left:auto;margin-right:auto;'\n"
 
     text = text .. "|- style='font-size:1.5em;text-align:left;' \n| colspan='6' |" .. locName .. "\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|" .. L.Attack .. " || " ..  tostring(stats["offence"]) .. " | " .. L.Tier .. " || " .. val(unit.tier)
            -- Строка с именем и описанием
    text = text .. '| rowspan="8" style="width:100px;" |Temple_Swordsman_Battlefield.png' .. '\n'
            local body = "<b>" .. ab.name .. "</b>"


    text = text .. "|-\n|" .. L.Defense .. " || " .. val(stats.defence) .. " || " .. L.Fraction .. " || " .. getLocFraction(unit.fraction, lang) .. "\n"
            -- Для активных способностей — ранг и стоимость
    text = text .. "|-\n|" .. L.Damage .. " || " .. val(stats.damageMin) .. "–" .. val(stats.damageMax) .. " || " .. L.Range .. " || " .. val(unit.ai) .. "\n"
            if not ab.isBaseClass then
    text = text .. "|-\n|" .. L.Health .. " || " .. val(stats.hp) .. " || " .. L.Movement .. " || " .. val(unit.moveType) .. "\n"
                local meta = {}
    text = text .. "|-\n|" .. L.Speed .. " || " .. val(stats.speed) .. " || " .. L.Price .. " || " .. val(cost.gold) .. " Gold\n"
                if ab.energyLevel and ab.energyLevel ~= "" then
    text = text .. "|-\n|" .. L.Initiative .. " || " .. val(stats.initiative) .. " || " .. L.Growth .. " || " .. val(unit.growth) .. "\n"
                    local costTpl = LangStrings.getText({ args = { sid = "tooltipsAbilityCost", lang = lang } }) or "Ability cost: {0}"
    text = text .. "|-\n|" .. L.Morale .. " || " .. val(stats.moral) .. " || " .. L.Experience .. " || " .. val(unit.expBonus) .. "\n"
                    local costStr = costTpl:gsub("{0}", ab.energyLevel)
    text = text .. "|-\n|" .. L.Luck .. " || " .. val(stats.luck) .. " || " .. L.SquadValue .. " || " .. val(unit.squadValue) .. "\n"
                    table.insert(meta, costStr)
                end
                if #meta > 0 then
                    body = body .. " <span style='color:gray;font-size:0.9em;'>| " .. table.concat(meta, " | ") .. "</span>"
                end
            end


    text = text .. "|-\n| colspan='6' |" .. locDesc .. "\n"
            body = body .. "<br>" .. (ab.description or "")


    -- Получаем локализованные имена всех способностей
            -- Ограничения (excaptionInTooltip)
    local abilityNames = {}
            if ab.excaption and ab.excaption ~= "" then
    for _, aid in ipairs(unit.abilities or {}) do
                body = body .. "<br><span style='color:#888;font-size:0.9em;font-style:italic;'>" .. ab.excaption .. "</span>"
        local name = LocGameEntities.getLocName({ args = { name = aid, lang = lang } })
            end


        if name and name ~= "" then
             text = text .. "|-\n| style='text-align:center;width:80px;vertical-align:middle;' | " .. icon ..
             local desctipion = LocGameEntities.getLocDescription({ args = { name = aid, lang = lang } })
                        " || style='padding:10px;' | " .. body .. "\n"
            table.insert(abilityNames, "<b>" .. name .. "</b> - " .. desctipion)
         end
         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
    name = name and name:gsub("'", "’") or name
    name = name:gsub("%_%(unit%)", "") -- for Stinger unit and Stinger hero
    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
     end


     local abilitiesText = table.concat(abilityNames, "<br /><hr /><br />")
    -- основной юнит
     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


     text = text .. "|-\n| colspan='6' |" .. (abilitiesText ~= "" and abilitiesText or "—") .. "\n"
     return text
end


     text = text .. "|}\n"
-- 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


    -- отладочный JSON
    -- local debugJson = "<pre>DEBUG JSON:\n" .. mw.text.jsonEncode(unit) .. "</pre>\n"
    -- text = text .. mw.text.jsonEncode(stats)


     return text 
-- NavBox for fractions
function p.navBox(frame)
    local faction = frame.args.faction
    local lang = frame.args.lang or "en"
    local langSuffix = lang ~= "en" and "/" .. lang or ""
   
    local result = '{| class="wikitable mw-collapsible white-toggle-button" style="width:100%;"\n'
    result = result .. '!colspan="8"|[[Units' .. langSuffix .. '|Units]]\n'
   
    -- Функция для получения перевода названия фракции
    local function getFactionName(fac)
        if fac == "Neutral" then
            return "Neutral"  -- нейтралы остаются на английском
        end
       
        local sidMap = {
            Temple = "world_cheat_dropdown_fraction_human",
            Necropolis = "world_cheat_dropdown_fraction_undead",
            Dungeon = "world_cheat_dropdown_fraction_dungeon",
            Sylvan = "world_cheat_dropdown_fraction_nature",
            Hive = "world_cheat_dropdown_fraction_demon",
            Schism = "world_cheat_dropdown_fraction_unfrozen"
        }
       
        local sid = sidMap[fac]
        if sid then
            return LangStrings.getText({ args = { sid = sid, lang = lang } }) or fac
        end
        return fac
    end
   
    -- Функция для создания ссылки на юнита с flex выравниванием
    local function getUnitLink(unitName)
        local locName = unitGet(unitName, "name", lang)
        return '<div style="display:flex; align-items:center; margin-bottom:4px;">[[File:' .. unitName:gsub(" ", "_") .. '_icon.png|28px|link=' .. unitName .. langSuffix .. ']]&nbsp;[[' .. unitName .. langSuffix .. '|' .. locName .. ']]</div>'
    end
   
    -- Функция для рендеринга одной фракции
    local function renderFaction(fac)
        local factionName = getFactionName(fac)
        local row = '|-\n'
        row = row .. '|align="center" style="vertical-align:middle;"|[[File:' .. factionIcons[fac] .. '|50px|link=' .. factionPages[fac] .. langSuffix .. '|' .. factionName .. ' Units]]<br>[[' .. factionPages[fac] .. langSuffix .. '|' .. factionName .. ']]\n'
       
        local units = factions[fac]
        if fac == "Neutral" then
            -- Особый формат для нейтральных юнитов
            row = row .. '|colspan="7" style="vertical-align:middle;"|'
            for i, unit in ipairs(units) do
                local locName = unitGet(unit, "name", lang)
                row = row .. '<div style="display:inline-flex; align-items:center; margin:0 8px 4px 0;">[[File:' .. unit:gsub(" ", "_") .. '_icon.png|28px|link=' .. unit .. langSuffix .. ']]&nbsp;[[' .. unit .. langSuffix .. '|' .. locName .. ']]</div>'
                if i < #units then
                    row = row .. ' • '
                end
            end
        else
            -- Обычные фракции (7 колонок по 3 юнита)
            row = row .. '| style="vertical-align:middle;"|'
            for i = 1, 21, 3 do
                if i > 1 then
                    row = row .. '\n| style="vertical-align:middle;"|'
                end
                for j = 0, 2 do
                    if units[i + j] then
                        row = row .. getUnitLink(units[i + j])
                    end
                end
            end
        end
        return row .. '\n'
    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
   
     return result .. '|}\n'
end
end


return p
return p

Latest revision as of 11:55, 10 July 2026

Documentation for this module may be created at Module:LocUnitData/doc

local p = {}

-- Подключаем зависимости
local LangStrings = require("Module:LocGetString")
local UnitModule  = require("Module:Unit")  -- наш новый модуль

-----------------------------------------------
-- ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ
-----------------------------------------------
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",
        "Armored 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_(unit)",
        "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 = "Neutral_faction.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"
}

-----------------------------------------------------
-- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
-----------------------------------------------------

-- Обёртка: вызвать Module:Unit get с нужными аргументами
local function unitGet(name, property, lang)
    return UnitModule.get({ args = { name, property, lang or "en" } })
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]]",
        AbilityRank = getLabel("tooltipRang", lang),
        EnergyCost  = getLabel("unit_energy", lang),
    }
end




function p.printUnitData(frame)
    local name = frame.args[1] or frame.args.name
    if name then
        name = name:gsub("'", "’")
        name = name:gsub("%_%(unit%)", "")
    else
        return "Ошибка: не указано имя юнита"
    end
    
    local lang = frame.args.lang or "en"
    
    local success, result = pcall(function()
        return p.getUnitData(name, lang)
    end)
    
    if not success then
        return "Ошибка: " .. tostring(result)
    end
    
    -- Простой вывод всей таблицы
    local output = "=== ДАННЫЕ ДЛЯ ОТЛАДКИ ===\n"
    output = output .. "Тип результата: " .. type(result) .. "\n\n"
    
    if type(result) == "table" then
        output = output .. "Содержимое таблицы:\n"
        for key, value in pairs(result) do
            output = output .. "  " .. tostring(key) .. " = " .. tostring(value) .. "\n"
        end
    else
        output = output .. "Результат: " .. tostring(result)
    end
    
    return "<pre>" .. output .. "</pre>"
end

-----------------------------------------------------
-- ОСНОВНАЯ ЛОГИКА: получение данных юнита
-----------------------------------------------------
function p.getUnitData(name, lang)
    if not name then return nil, "No unit name provided" end
    lang = lang or "en"

    -- Проверяем что юнит существует
    local idCheck = unitGet(name, "id", lang)
    if not idCheck or idCheck == "" or idCheck:find("^Unit '") then
        return nil, "Unit not found: " .. name
    end

    -- Базовые поля через Module:Unit
    local locName    = unitGet(name, "name",            lang)
    local locDesc    = unitGet(name, "description",     lang)
    local fraction   = unitGet(name, "faction",         lang)
    local tier       = unitGet(name, "tier",            lang)  -- уже римские цифры
    local cost       = unitGet(name, "cost",            lang)
    local growth     = unitGet(name, "weeklyIncrement", lang)
    local expBonus   = unitGet(name, "expBonus",        lang)
    local squadValue = unitGet(name, "squadValue",      lang)
    local imageName  = unitGet(name, "imageName",       "en")  -- без пробелов/апострофов

    -- Линейка апгрейдов (имена-ключи на en, для обхода в getUnits)
    local baseUnitName = unitGet(name, "baseName",       "en")
    local upgUnitName  = unitGet(name, "upgradeName",    "en")
    local altUnitName  = unitGet(name, "altUpgradeName", "en")

    -- Статы (числовые поля)
    local statFields = {
        "hp", "offence", "defence", "damageMin", "damageMax",
        "speed", "initiative", "moral", "luck"
    }
    local stats = {}
    for _, f in ipairs(statFields) do
        stats[f] = unitGet(name, f, lang)
    end

    -- moveType и тип атаки
    local moveType   = unitGet(name, "moveType", lang)
    local attack_type = ""

    -- Способности
    local abilities = {}

    -- BaseClass — добавляем первым
    local bcName = unitGet(name, "baseClass.name", lang)
    if bcName and bcName ~= "" then
        local bcDesc = unitGet(name, "baseClass.description", lang)
        local bcIcon = unitGet(name, "baseClass.icon", lang)
        table.insert(abilities, { name = bcName, description = bcDesc, icon = bcIcon, isBaseClass = true })
    end

    -- Активные способности
    local i = 1
    while true do
        local aname = unitGet(name, "abilities." .. i .. ".name", lang)
        if not aname or aname == "" then break end
        local adesc        = unitGet(name, "abilities." .. i .. ".description",        lang)
        local aicon        = unitGet(name, "abilities." .. i .. ".icon",               lang)
        local atype        = unitGet(name, "abilities." .. i .. ".type",               lang)
        local ability_tier = unitGet(name, "abilities." .. i .. ".ability_tier",       lang)
        local energyLevel  = unitGet(name, "abilities." .. i .. ".energyLevel",        lang)
        local excaption    = unitGet(name, "abilities." .. i .. ".excaptionInTooltip", lang)
        table.insert(abilities, {
            name         = aname,
            description  = adesc,
            icon         = aicon,
            atype        = atype,
            ability_tier = ability_tier,
            energyLevel  = energyLevel,
            excaption    = excaption,
        })
        if atype == "attack_melee" or atype == "attack_range" or atype == "attack_shoot" then
            attack_type = aname
        end
        i = i + 1
    end

    -- Пассивки (добавляем к способностям)
    i = 1
    while true do
        local aname = unitGet(name, "passives." .. i .. ".name", lang)
        if not aname or aname == "" then break end
        local adesc = unitGet(name, "passives." .. i .. ".description", lang)
        local aicon = unitGet(name, "passives." .. i .. ".icon", lang)
        table.insert(abilities, { name = aname, description = adesc, icon = aicon })
        i = i + 1
    end

    -- Изображение: Faction_ImageName_Battlefield.png
    local factionRaw = unitGet(name, "faction", "en")  -- внутренний ключ (human, nature, ...)
    local factionCap = factionRaw ~= "" and (factionRaw:sub(1,1):upper() .. factionRaw:sub(2)) or ""
    local image = factionCap .. "_" .. imageName .. "_Battlefield.png"

    return {
        id          = idCheck,
        upgradeLine = { baseUnitName, upgUnitName, altUnitName },
        name        = name,
        locName     = locName,
        locDesc     = locDesc,
        image       = image,
        tier        = tier,
        fraction    = fraction,
        attack_type = attack_type,
        stats       = stats,
        cost        = cost,
        growth      = growth,
        moveType    = moveType,
        expBonus    = expBonus,
        squadValue  = 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]]"

            -- Строка с именем и описанием
            local body = "<b>" .. ab.name .. "</b>"

            -- Для активных способностей — ранг и стоимость
            if not ab.isBaseClass then
                local meta = {}
                if ab.energyLevel and ab.energyLevel ~= "" then
                    local costTpl = LangStrings.getText({ args = { sid = "tooltipsAbilityCost", lang = lang } }) or "Ability cost: {0}"
                    local costStr = costTpl:gsub("{0}", ab.energyLevel)
                    table.insert(meta, costStr)
                end
                if #meta > 0 then
                    body = body .. " <span style='color:gray;font-size:0.9em;'>| " .. table.concat(meta, " | ") .. "</span>"
                end
            end

            body = body .. "<br>" .. (ab.description or "")

            -- Ограничения (excaptionInTooltip)
            if ab.excaption and ab.excaption ~= "" then
                body = body .. "<br><span style='color:#888;font-size:0.9em;font-style:italic;'>" .. ab.excaption .. "</span>"
            end

            text = text .. "|-\n| style='text-align:center;width:80px;vertical-align:middle;' | " .. icon ..
                        " || style='padding:10px;' | " .. body .. "\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
    name = name and name:gsub("'", "’") or name
    name = name:gsub("%_%(unit%)", "") -- for Stinger unit and Stinger hero
    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 langSuffix = lang ~= "en" and "/" .. lang or ""
    
    local result = '{| class="wikitable mw-collapsible white-toggle-button" style="width:100%;"\n'
    result = result .. '!colspan="8"|[[Units' .. langSuffix .. '|Units]]\n'
    
    -- Функция для получения перевода названия фракции
    local function getFactionName(fac)
        if fac == "Neutral" then
            return "Neutral"  -- нейтралы остаются на английском
        end
        
        local sidMap = {
            Temple = "world_cheat_dropdown_fraction_human",
            Necropolis = "world_cheat_dropdown_fraction_undead", 
            Dungeon = "world_cheat_dropdown_fraction_dungeon",
            Sylvan = "world_cheat_dropdown_fraction_nature",
            Hive = "world_cheat_dropdown_fraction_demon",
            Schism = "world_cheat_dropdown_fraction_unfrozen"
        }
        
        local sid = sidMap[fac]
        if sid then
            return LangStrings.getText({ args = { sid = sid, lang = lang } }) or fac
        end
        return fac
    end
    
    -- Функция для создания ссылки на юнита с flex выравниванием
    local function getUnitLink(unitName)
        local locName = unitGet(unitName, "name", lang)
        return '<div style="display:flex; align-items:center; margin-bottom:4px;">[[File:' .. unitName:gsub(" ", "_") .. '_icon.png|28px|link=' .. unitName .. langSuffix .. ']]&nbsp;[[' .. unitName .. langSuffix .. '|' .. locName .. ']]</div>'
    end
    
    -- Функция для рендеринга одной фракции
    local function renderFaction(fac)
        local factionName = getFactionName(fac)
        local row = '|-\n'
        row = row .. '|align="center" style="vertical-align:middle;"|[[File:' .. factionIcons[fac] .. '|50px|link=' .. factionPages[fac] .. langSuffix .. '|' .. factionName .. ' Units]]<br>[[' .. factionPages[fac] .. langSuffix .. '|' .. factionName .. ']]\n'
        
        local units = factions[fac]
        if fac == "Neutral" then
            -- Особый формат для нейтральных юнитов
            row = row .. '|colspan="7" style="vertical-align:middle;"|'
            for i, unit in ipairs(units) do
                local locName = unitGet(unit, "name", lang)
                row = row .. '<div style="display:inline-flex; align-items:center; margin:0 8px 4px 0;">[[File:' .. unit:gsub(" ", "_") .. '_icon.png|28px|link=' .. unit .. langSuffix .. ']]&nbsp;[[' .. unit .. langSuffix .. '|' .. locName .. ']]</div>'
                if i < #units then
                    row = row .. ' • '
                end
            end
        else
            -- Обычные фракции (7 колонок по 3 юнита)
            row = row .. '| style="vertical-align:middle;"|'
            for i = 1, 21, 3 do
                if i > 1 then
                    row = row .. '\n| style="vertical-align:middle;"|'
                end
                for j = 0, 2 do
                    if units[i + j] then
                        row = row .. getUnitLink(units[i + j])
                    end
                end
            end
        end
        return row .. '\n'
    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
    
    return result .. '|}\n'
end

return p