Module:LocUnitData: Difference between revisions

From Heroes of Might and Magic: Olden Era Official Wiki
No edit summary
No edit summary
Line 562: Line 562:
     local faction = frame.args.faction
     local faction = frame.args.faction
     local lang = frame.args.lang or "en"
     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'
     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'
     result = result .. '!colspan=8|[[Units' .. langSuffix .. '|Units]]\n'
      
      
     if faction and factions[faction] then
     local function getUnitLink(unitName)
        -- Показываем только одну фракцию
        local locName = faction == "Neutral" and unitName or
         result = result .. '|-\n'
                      LocGameEntities.getLocName({ args = { name = unitName, lang = lang } })
         result = result .. '|align="center"|[[File:' .. factionIcons[faction] .. '|50px|link=' .. factionPages[faction] .. (lang ~= "en" and "/" .. lang or "") .. '|' .. factionNames[faction] .. ' Units]]<br>[[' .. factionPages[faction] .. (lang ~= "en" and "/" .. lang or "") .. '|' .. factionNames[faction] .. ']]\n'
        return '[[File:' .. unitName:gsub(" ", "_") .. '_icon.png|28px|link=' .. unitName .. langSuffix .. ']] [[' ..
         result = result .. '| '
              unitName .. langSuffix .. '|' .. locName .. ']]'
    end
   
    local function renderFactionRow(fac)
         local row = '|-\n'
         row = row .. '|align="center"|[[File:' .. factionIcons[fac] .. '|50px|link=' ..  
              factionPages[fac] .. langSuffix .. '|' .. factionNames[fac] .. ' Units]]<br>[[' ..  
              factionPages[fac] .. langSuffix .. '|' .. factionNames[fac] .. ']]\n'
          
        local units = factions[fac]
          
          
        local units = factions[faction]
         if fac == "Neutral" then
         if faction == "Neutral" then
             row = row .. '|colspan=7|'
             -- Особый формат для нейтральных юнитов
            local unitLinks = {}
             for i, unit in ipairs(units) do
             for _, unit in ipairs(units) do
                 result = result .. '[[File:' .. unit:gsub(" ", "_") .. '_icon.png|28px|link=' .. unit .. (lang ~= "en" and "/" .. lang or "") .. ']] [[' .. unit .. (lang ~= "en" and "/" .. lang or "") .. '|' .. unit .. ']]'
                 table.insert(unitLinks, getUnitLink(unit))
                if i < #units then
                    result = result .. ' • '
                end
             end
             end
            row = row .. table.concat(unitLinks, ' • ')
         else
         else
            -- Обычные фракции (7 колонок по 3 юнита)
             for i = 1, 21, 3 do
             for i = 1, 21, 3 do
                 if i > 1 then
                 if i > 1 then row = row .. '\n| ' end
                    result = result .. '\n| '
               
                 end
                 local groupUnits = {}
                 for j = 0, 2 do
                 for j = 0, 2 do
                     if units[i + j] then
                     if units[i + j] then
                         result = result .. '[[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 "") .. '|' .. units[i + j] .. ']]'
                         table.insert(groupUnits, getUnitLink(units[i + j]))
                        if j < 2 and units[i + j + 1] then
                            result = result .. ' <br> '
                        end
                     end
                     end
                 end
                 end
                row = row .. table.concat(groupUnits, ' <br> ')
             end
             end
         end
         end
       
        return row
    end
   
    if faction and factions[faction] then
        -- Показываем одну фракцию
        result = result .. renderFactionRow(faction)
     else
     else
         -- Показываем все фракции (оригинальный вид)
         -- Показываем все фракции
         local allFactions = {"Temple", "Necropolis", "Dungeon", "Schism", "Sylvan", "Hive", "Neutral"}
         for _, fac in ipairs({"Temple", "Necropolis", "Dungeon", "Schism", "Sylvan", "Hive", "Neutral"}) do
       
             result = result .. renderFactionRow(fac)
        for _, fac in ipairs(allFactions) do
             result = result .. '|-\n'
            result = result .. '|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
                result = result .. '|colspan=7|'
                for i, unit in ipairs(units) do
                    result = result .. '[[File:' .. unit:gsub(" ", "_") .. '_icon.png|28px|link=' .. unit .. (lang ~= "en" and "/" .. lang or "") .. ']] [[' .. unit .. (lang ~= "en" and "/" .. lang or "") .. '|' .. unit .. ']]'
                    if i < #units then
                        result = result .. ' • '
                    end
                end
            else
                -- 7 колонок по 3 юнита
                for i = 1, 21, 3 do
                    if i > 1 then
                        result = result .. '\n| '
                    end
                    for j = 0, 2 do
                        if units[i + j] then
                            local locName = LocGameEntities.getLocName({ args = { name = units[i + j], lang = lang } })
                            result = result .. '[[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
                                result = result .. ' <br> '
                            end
                        end
                    end
                end
            end
            result = result .. '\n'
         end
         end
     end
     end
      
      
     result = result .. '|}\n'
     result = result .. '|}\n'
   
     return result
     return result
end
end


return p
return p

Revision as of 11:52, 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 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 getUnitLink(unitName)
        local locName = faction == "Neutral" and unitName or 
                       LocGameEntities.getLocName({ args = { name = unitName, lang = lang } })
        return '[[File:' .. unitName:gsub(" ", "_") .. '_icon.png|28px|link=' .. unitName .. langSuffix .. ']] [[' .. 
               unitName .. langSuffix .. '|' .. locName .. ']]'
    end
    
    local function renderFactionRow(fac)
        local row = '|-\n'
        row = row .. '|align="center"|[[File:' .. factionIcons[fac] .. '|50px|link=' .. 
               factionPages[fac] .. langSuffix .. '|' .. factionNames[fac] .. ' Units]]<br>[[' .. 
               factionPages[fac] .. langSuffix .. '|' .. factionNames[fac] .. ']]\n'
        
        local units = factions[fac]
        
        if fac == "Neutral" then
            row = row .. '|colspan=7|'
            local unitLinks = {}
            for _, unit in ipairs(units) do
                table.insert(unitLinks, getUnitLink(unit))
            end
            row = row .. table.concat(unitLinks, ' • ')
        else
            for i = 1, 21, 3 do
                if i > 1 then row = row .. '\n| ' end
                
                local groupUnits = {}
                for j = 0, 2 do
                    if units[i + j] then
                        table.insert(groupUnits, getUnitLink(units[i + j]))
                    end
                end
                row = row .. table.concat(groupUnits, ' <br> ')
            end
        end
        
        return row
    end
    
    if faction and factions[faction] then
        -- Показываем одну фракцию
        result = result .. renderFactionRow(faction)
    else
        -- Показываем все фракции
        for _, fac in ipairs({"Temple", "Necropolis", "Dungeon", "Schism", "Sylvan", "Hive", "Neutral"}) do
            result = result .. renderFactionRow(fac)
        end
    end
    
    result = result .. '|}\n'
    return result
end

return p