Module:Unit: Difference between revisions

From Heroes of Might and Magic: Olden Era Official Wiki
No edit summary
No edit summary
 
(45 intermediate revisions by 2 users not shown)
Line 3: Line 3:
-- Cache for data
-- Cache for data
local dataCache = nil
local dataCache = nil
-- Cache for reverse lookup: id -> unitName
local idIndexCache = nil


-- Load localization module
-- Load localization module
local LangStrings = require("Module:LocGetString")
local LangStrings = require("Module:LocGetString")
-- Roman numeral conversion (1–20 should cover all tiers)
local function toRoman(n)
    n = tonumber(n)
    if not n or n <= 0 then return tostring(n or "") end
    local vals  = {1000,900,500,400,100,90,50,40,10,9,5,4,1}
    local syms  = {"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"}
    local result = ""
    for i, v in ipairs(vals) do
        while n >= v do
            result = result .. syms[i]
            n = n - v
        end
    end
    return result
end
-- Fallback display names used when localization returns empty.
-- Для neutral локализации нет — задаём вручную для всех языков.
local FACTION_DISPLAY = {
    demon    = "Hive",
    dungeon  = "Dungeon",
    human    = "Temple",
    nature  = "Grove",
    undead  = "Necropolis",
    unfrozen = "Schism",
}
local NEUTRAL_BY_LANG = {
    en = "Neutral",
    ru = "Нейтральные",
    pl = "Neutralne",
    fr = "Neutre",
}


-- Function to load JSON data from wiki page
-- Function to load JSON data from wiki page
Line 21: Line 57:
     if dataCache then return dataCache end
     if dataCache then return dataCache end
     dataCache = {}
     dataCache = {}
    idIndexCache = {}


    -- Load main JSON file (in real wiki, JSON data needs to be placed on this page)
     local units = loadJson("Data:units.json")
     local units = loadJson("Data:units.json") -- or another suitable name for wiki page


    -- Copy data to cache
     for name, unit in pairs(units) do
     for name, unit in pairs(units) do
         dataCache[name] = unit
         dataCache[name] = unit
        -- Build reverse index: id -> display name (key in the JSON object)
        if unit.id and unit.id ~= "" then
            idIndexCache[unit.id] = name
        end
     end
     end


Line 33: Line 72:
end
end


-- Function to get unit property
-- Return the reverse-index cache (loads units first if needed)
local function getIdIndex()
    if not idIndexCache then loadUnits() end
    return idIndexCache
end
 
-- Function to get localized text
local function getLocalizedText(sid, lang, returnEmptyIfMissing)
    if sid and sid ~= "" then
        local result = LangStrings.getText({args = {sid = sid, lang = lang}})
        return result
    else
        if returnEmptyIfMissing then
            return ""
        else
            return sid
        end
    end
end
 
-- Function to get localized description with data substitutions
local function getLocalizedDescription(item, lang)
    if not item or not item.sid_desc then
        return ""
    end
 
    local text = LangStrings.getText({ args = { sid = item.sid_desc, lang = lang } }) or ""
    local data = item.data or {}
 
    local replacements = {}
 
    if item.data_sid and type(item.data_sid) == "table" then
        for i, sid in ipairs(item.data_sid) do
            if sid and sid ~= "" then
                local part = LangStrings.getText({ args = { sid = sid, lang = lang } }) or ""
                if part ~= "" then
                    if type(data[i]) == "table" then
                        part = part:gsub("{(%d+)}", function(n)
                            local idx = tonumber(n) + 1
                            local val = data[i][idx]
                            return val ~= nil and tostring(val) or "{" .. n .. "}"
                        end)
                        replacements[i] = part
                    else
                        part = part:gsub("{0}", tostring(data[i]))
                        replacements[i] = part
                    end
                end
            elseif data[i] ~= nil then
                replacements[i] = tostring(data[i])
            end
        end
 
        for i = #item.data_sid + 1, #data do
            if data[i] ~= nil then
                replacements[i] = tostring(data[i])
            end
        end
    else
        for i, val in ipairs(data) do
            replacements[i] = tostring(val)
        end
    end
 
    text = text:gsub("{(%d+)}", function(n)
        local idx = tonumber(n) + 1
        local val = replacements[idx]
        return val or "{" .. n .. "}"
    end)
 
    return text
end
 
-- Return the display name of a unit (with localization)
local function getUnitName(unit, lang)
    if unit.sid_name and unit.sid_name ~= "" then
        return getLocalizedText(unit.sid_name, lang, false)
    end
    local id = unit.id or ""
    if id ~= "" then
        return getLocalizedText(id .. "_name", lang, false)
    end
    return unit.name or "No name"
end
 
-- Return the display name of an upgrade/base unit by its id string.
-- Returns "" if the id is nil, empty, or no matching unit exists.
local function getNameById(id, lang)
    if not id or id == "" then return "" end
    local idx = getIdIndex()
    local unitName = idx[id]
    if not unitName then return "" end
    local units = loadUnits()
    local unit = units[unitName]
    if not unit then return "" end
    local displayName = getUnitName(unit, lang)
    -- Remove apostrophes, replace spaces with nothing
    displayName = displayName:gsub("’", "'")
    return displayName
end
 
-- Helper: get unitCost property
local function getUnitCostProperty(unitCost, propName)
    if unitCost == nil or type(unitCost) ~= "table" then return "" end
    local value = unitCost[propName]
    return value ~= nil and tostring(value) or ""
end
 
-- Helper: format a cost table as wiki markup
local function formatCost(unitCost)
    if unitCost == nil or type(unitCost) ~= "table" then return "" end
 
    local result = {}
 
    if unitCost.gold and type(unitCost.gold) == "number" and unitCost.gold > 0 then
        table.insert(result, tostring(unitCost.gold) ..
            "[[File:Icon Resource Gold.png|32px|link=Resources#Gold|Gold]]")
    end
 
    for resName, resValue in pairs(unitCost) do
        if resName ~= "gold" and type(resValue) == "number" and resValue > 0 then
            local resNameUpper = string.upper(string.sub(resName, 1, 1)) .. string.sub(resName, 2)
            table.insert(result, tostring(resValue) ..
                "[[File:Icon Resource " .. resNameUpper .. ".png|32px|link=Resources#" ..
                resNameUpper .. "|" .. resNameUpper .. "]]")
        end
    end
 
    return table.concat(result, " ")
end
 
-- Helper: process a sub-object (array element or baseClass) for a property request
local function getSubObjectProperty(element, propName, lang)
    if element == nil then return "" end
    if type(element) ~= "table" then return "" end
 
    -- 'name' – prefer sid_name
    if propName == "name" then
        if element["sid_name"] then
            return getLocalizedText(element["sid_name"], lang, false)
        elseif element["name"] then
            return tostring(element["name"])
        else
            return ""
        end
    end
 
    -- 'description' – use sid_desc with data substitution
    if propName == "description" then
        if element["sid_desc"] then
            return getLocalizedDescription(element, lang)
        else
            return ""
        end
    end
 
    local value = element[propName]
    if value ~= nil then
        -- Localized fields that should return empty when the sid is missing
        if propName == "abilityType" or propName == "excaptionInTooltip" or propName == "infoDescription" then
            return getLocalizedText(value, lang, true)
        else
            return tostring(value)
        end
    end
 
    return ""
end
 
-- ============================================================
-- Main exported function
-- ============================================================
function Unit.get(frame)
function Unit.get(frame)
     local args = frame.args
     local args = frame.args
     local unitName = args[1] or args.unit or ""
     local unitName = mw.text.trim(args[1] or args.unit or "")
     local property = args[2] or args.property or ""
     local property = mw.text.trim(args[2] or args.property or "")
     local lang = args[3] or args.lang or "en"  -- Get language parameter, default to 'en'
     local lang     = mw.text.trim(args[3] or args.lang or "en")
 
    if unitName == "" then return "Unit name not specified" end
    if property == "" then return "Property not specified" end
 
    local units = loadUnits()
    local unit = units[unitName]
    if not unit then return "Unit '" .. unitName .. "' not found" end
 
    -- --------------------------------------------------------
    -- tier  →  Roman numerals
    -- --------------------------------------------------------
    if property == "tier" then
        local t = unit["tier"]
        if t == nil and unit.stats then t = unit.stats["tier"] end
        return t ~= nil and toRoman(t) or ""
    end
 
    -- --------------------------------------------------------
    -- name
    -- --------------------------------------------------------
    if property == "name" then
        local displayName = getUnitName(unit, lang)
        -- Remove apostrophes, replace spaces with nothing
        displayName = displayName:gsub("’", "'")
        return displayName
    end
 
    -- --------------------------------------------------------
    -- faction  →  localized faction name
    -- --------------------------------------------------------
    if property == "faction" then
        local factionKey = unit["faction"]
        if not factionKey or factionKey == "" then return "" end
        -- neutral отсутствует в локализации — возвращаем хардкод по языку
        if factionKey == "neutral" then
            return NEUTRAL_BY_LANG[lang] or NEUTRAL_BY_LANG["en"]
        end
        local loc = getLocalizedText("world_cheat_dropdown_fraction_" .. factionKey, lang, true)
        if loc and loc ~= "" then return loc end
        return FACTION_DISPLAY[factionKey] or factionKey
    end
 
    -- --------------------------------------------------------
    -- Вспомогательная: получить базовый id цепочки.
    -- phoenix_upg_alt -> phoenix
    -- phoenix_upg    -> phoenix
    -- phoenix        -> phoenix
    -- --------------------------------------------------------
    local function getBaseId()
        local myId = unit["id"] or ""
        -- Сначала пробуем убрать длинный суффикс, потом короткий
        local base = myId:match("^(.+)_upg_alt$") or myId:match("^(.+)_upg$") or myId
        return base
    end


     unitName = mw.text.trim(unitName or "")
     -- --------------------------------------------------------
     property = mw.text.trim(property or "")
    -- baseName  ->  имя базового существа цепочки (всегда одно и то же
    lang = mw.text.trim(lang or "en")
    --              для базы, грейда и альтгрейда)
     -- --------------------------------------------------------
    if property == "baseName" then
        return getNameById(getBaseId(), lang)
    end


     if unitName == "" then
    -- --------------------------------------------------------
         return "Unit name not specified"
    -- upgradeName  ->  имя первого апгрейда (baseId_upg)
    --                  одинаково для любого юнита цепочки
    -- --------------------------------------------------------
     if property == "upgradeName" then
         return getNameById(getBaseId() .. "_upg", lang)
     end
     end


     if property == "" then
    -- --------------------------------------------------------
         return "Property not specified"
    -- altUpgradeName  ->  имя альт-апгрейда (baseId_upg_alt)
    --                    одинаково для любого юнита цепочки
    -- --------------------------------------------------------
     if property == "altUpgradeName" then
         return getNameById(getBaseId() .. "_upg_alt", lang)
     end
     end


     -- Load unit data
     -- --------------------------------------------------------
     local units = loadUnits()
     -- upgradeCost / upgradeCost.propertyName
    -- Разница в стоимости между апгрейднутым существом и базовым.
    -- Одинакова для обоих апгрейдов (alt и обычного), т.к. оба
    -- стоят одинаково, а базовый юнит — тот, чей upgradeSid == id текущего.
    -- Для базового существа (у которого нет предшественника) — пустая строка.
    -- --------------------------------------------------------


     -- Check if unit exists in data
     -- Найти базовый юнит через стрипинг суффикса id (та же логика что getBaseId)
     local unit = units[unitName]
     local function findBaseUnit()
    if not unit then
        local myId = unit["id"] or ""
         return "Unit '" .. unitName .. "' not found"
        if myId == "" then return nil end
         local baseId = myId:match("^(.+)_upg_alt$") or myId:match("^(.+)_upg$")
        if not baseId then return nil end  -- уже базовый, нет предшественника
        local idx = getIdIndex()
        local baseName = idx[baseId]
        if not baseName then return nil end
        return units[baseName]
     end
     end


     -- Check if property is in format "arrayName.index.propertyName" (for accessing arrays)
     -- Вычислить разницу unitCost: текущий минус базовый
     local arrayAccess = string.match(property, "^(.+)%.(%d+)%.(.+)$")
     -- Возвращает таблицу {resource -> delta} только с положительными значениями
     if arrayAccess then
     local function calcUpgradeCostDiff()
        local arrayName, indexStr, propName = string.match(property, "^(.+)%.(%d+)%.(.+)$")
         local baseUnit = findBaseUnit()
         local index = tonumber(indexStr)
         if not baseUnit then return nil end
         if arrayName and index and propName then
        local myCost  = unit["unitCost"]   or {}
            -- Convert from 1-based indexing (user-friendly) to 0-based indexing (JSON format)
        local baseCost = baseUnit["unitCost"] or {}
            local arrayIndex = index - 1
        local diff = {}
            if unit[arrayName] and unit[arrayName][arrayIndex] then
        local allKeys = {}
                local arrayItem = unit[arrayName][arrayIndex]
        for k in pairs(myCost)  do allKeys[k] = true end
                if arrayItem[propName] ~= nil then
        for k in pairs(baseCost) do allKeys[k] = true end
                    return tostring(arrayItem[propName])
        for k in pairs(allKeys) do
                else
            local delta = (myCost[k] or 0) - (baseCost[k] or 0)
                    return "Property '" .. propName .. "' not found in " .. arrayName .. "[" .. arrayIndex .. "] for unit '" .. unitName .. "'"
             if delta > 0 then
                end
                 diff[k] = delta
             else
                 return "Array '" .. arrayName .. "[" .. arrayIndex .. "]' not found for unit '" .. unitName .. "'"
             end
             end
         end
         end
        return diff
    end
    if property == "upgradeCost" then
        local diff = calcUpgradeCostDiff()
        if not diff then return "" end
        return formatCost(diff)
    end
    local upgradeCostAccess = string.match(property, "^upgradeCost%.(.+)$")
    if upgradeCostAccess then
        local diff = calcUpgradeCostDiff()
        if not diff then return "" end
        local value = diff[upgradeCostAccess]
        return value ~= nil and tostring(value) or ""
    end
    -- --------------------------------------------------------
    -- imageName  →  unit display name without spaces and apostrophes,
    --              suitable for wiki file names
    -- --------------------------------------------------------
    if property == "imageName" then
        local displayName = getUnitName(unit, lang)
        -- Remove apostrophes, replace spaces with nothing
        displayName = displayName:gsub("['’ ]", "")
        return displayName
    end
    -- --------------------------------------------------------
    -- cost  →  formatted hire cost
    -- --------------------------------------------------------
    if property == "cost" then
        return formatCost(unit["unitCost"])
    end
    -- cost.propertyName / unitCost.propertyName
    local costAccess = string.match(property, "^cost%.(.+)$")
    if costAccess then return getUnitCostProperty(unit["unitCost"], costAccess) end
    local unitCostAccess = string.match(property, "^unitCost%.(.+)$")
    if unitCostAccess then return getUnitCostProperty(unit["unitCost"], unitCostAccess) end
    -- --------------------------------------------------------
    -- arrayName.index.propertyName  (e.g. abilities.1.name)
    -- --------------------------------------------------------
    local arrayName, indexStr, propName = string.match(property, "^(.+)%.(%d+)%.(.+)$")
    if arrayName and indexStr and propName then
        local index = tonumber(indexStr)
        local array = unit[arrayName]
        if array == nil or type(array) ~= "table" then return "" end
        local element = array[index]
        if element == nil then return "" end
        return getSubObjectProperty(element, propName, lang)
    end
    -- --------------------------------------------------------
    -- baseClass.propertyName
    -- --------------------------------------------------------
    local baseClassAccess = string.match(property, "^baseClass%.(.+)$")
    if baseClassAccess then
        local baseClass = unit["baseClass"]
        if baseClass == nil or type(baseClass) ~= "table" then return "" end
        return getSubObjectProperty(baseClass, baseClassAccess, lang)
     end
     end


     -- First check if property exists in main unit object
     -- --------------------------------------------------------
    -- Direct property on unit object
    -- --------------------------------------------------------
     if unit[property] ~= nil then
     if unit[property] ~= nil then
         return tostring(unit[property])
         if property == "name" and unit["sid_name"] then
            return getLocalizedText(unit["sid_name"], lang, false)
        elseif property == "description" and unit["sid_desc"] then
            return getLocalizedDescription(unit, lang)
        else
            return tostring(unit[property])
        end
    end
 
    -- description via id
    if property == "description" then
        local id = unit.id or ""
        if id ~= "" then
            return getLocalizedText(id .. "_narrativeDescription", lang, true)
        end
        return ""
     end
     end


     -- Then check if property exists in stats
     -- Fallback: check stats sub-table
     if unit.stats and unit.stats[property] ~= nil then
     if unit.stats and unit.stats[property] ~= nil then
         return tostring(unit.stats[property])
         return tostring(unit.stats[property])
     end
     end


    -- If property not found
     return ""
     return "Property '" .. property .. "' not found for unit '" .. unitName .. "'"
end
end


return Unit
return Unit

Latest revision as of 06:33, 25 May 2026

Module for requesting characteristics of all existing units in Olden Era. Actual data is stored in Data:units.json.

For documentation page, see Module:Unit/doc.

Supported Commands

get

The module supports the main get command, through which you can retrieve individual unit attributes (listed below). The basic syntax of the command is:

{{#invoke:Unit|get|Unit Name|Property Name}}
Name Description Example Result
name Unit name (localized)
{{#invoke:Unit|get|Angel|name}}
Angel
Base name Name of un-upgraded unit (localized)
{{#invoke:Unit|get|Apotheosis|baseName}}
Angel
First Upgrade Name of upgraded unit (localized)
{{#invoke:Unit|get|Angel|upgradeName}}
Archangel
Second Upgrade Name of alt. upgraded unit (localized)
{{#invoke:Unit|get|Angel|altUpgradeName}}
Apotheosis
Image name Name without spaces and apostrophes, suitable for wiki file names
{{#invoke:Unit|get|Sun's Aegis|imageName}}
SunsAegis
description Narrative description (localized with data substitutions)
{{#invoke:Unit|get|Angel|description}}
The inquisition’s greatest belief is that angels still accompany their mission. The beings that aid the Templars in combat are but a mere imitation — a human soul willingly fused with winged armor, destined to continue the fight.
faction Unit faction
{{#invoke:Unit|get|Angel|faction}}
Temple
tier Unit tier
{{#invoke:Unit|get|Angel|tier}}
VII
id Unit ID
{{#invoke:Unit|get|Angel|id}}
angel
nativeBiome Native biome
{{#invoke:Unit|get|Angel|nativeBiome}}
Grass
cost Hiring cost
{{#invoke:Unit|get|Angel|cost}}
2400Gold 1Gemstones
cost.gold
cost.resource
Access to specific property in unitCost object
{{#invoke:Unit|get|Angel|cost.gold}}
{{#invoke:Unit|get|Angel|cost.gemstones}}
2400
1
upgradeCost Upgrade cost
{{#invoke:Unit|get|Archangel|upgradeCost}}
1600Gold 1Gemstones
weeklyIncrement Base weekly increment from main building
{{#invoke:Unit|get|Angel|weeklyIncrement}}
1
expBonus Experience gained when defeating the unit
{{#invoke:Unit|get|Angel|expBonus}}
366
squadValue Squad value of the unit (used for army composition calculations)
{{#invoke:Unit|get|Angel|squadValue}}
3660
hp Maximum health
{{#invoke:Unit|get|Angel|hp}}
225
offence Offence parameter
{{#invoke:Unit|get|Angel|offence}}
30
defence Defence parameter
{{#invoke:Unit|get|Angel|defence}}
30
damageMin Minimum damage dealt
{{#invoke:Unit|get|Angel|damageMin}}
50
damageMax Maximum damage dealt
{{#invoke:Unit|get|Angel|damageMax}}
75
moral Default morale
{{#invoke:Unit|get|Angel|moral}}
0
moralMin Minimum morale value
{{#invoke:Unit|get|Angel|moralMin}}
-3
moralMax Maximum morale value
{{#invoke:Unit|get|Angel|moralMax}}
3
luck Default luck
{{#invoke:Unit|get|Angel|luck}}
0
luckMin Minimum luck value
{{#invoke:Unit|get|Angel|luckMin}}
-3
luckMax Maximum luck value
{{#invoke:Unit|get|Angel|luckMax}}
3
initiative Initiative
{{#invoke:Unit|get|Angel|initiative}}
8
speed Speed
{{#invoke:Unit|get|Angel|speed}}
4
moveType Movement type (fly or teleport)
{{#invoke:Unit|get|Angel|moveType}}
fly
baseClass.name Base class name (localized)
{{#invoke:Unit|get|Angel|baseClass.name|ru}}
Embodiment
baseClass.description Base class description (localized)
{{#invoke:Unit|get|Angel|baseClass.description}}
A spirit, soul, or other intangible power that has coalesced into material form.

Morale range: –3 to -3.
Luck range: –3 to -3.

Fallen Embodiments do NOT count towards the power of Necromancy reanimation after battle.
baseClass.icon Base class icon filename
{{#invoke:Unit|get|Angel|baseClass.icon}} or [[File:{{#invoke:Unit|get|Angel|baseClass.icon}}|50px]]
Base class embodiment.png -

Active Abilities

To access specific active ability properties, use the format: abilities.index.property

Name Description Example Result
abilities.1.name Ability name (localized)
{{#invoke:Unit|get|Flaming Phoenix|abilities.1.name|ru}}
Пробуждение. Огонь II
abilities.1.description Ability main description (localized with data substitutions)
{{#invoke:Unit|get|Flaming Phoenix|abilities.1.description|ru}}
Получает +1 к скорости и инициативе, +25 к здоровью и всегда наносит максимальный урон. Позволяет использовать остальные способности. Применяется один раз за бой и действует до конца боя.
abilities.1.infoDescription Additional ability information (localized)
{{#invoke:Unit|get|Flaming Phoenix|abilities.1.infoDescription|ru}}
Не завершает ход.
abilities.1.excaptionInTooltip Ability restrictions or exceptions (localized)
{{#invoke:Unit|get|Cultist|abilities.1.excaptionInTooltip|ru}}
Не действует на магических существ, нежить и конструкты
abilities.1.abilityType Type of ability (localized)
{{#invoke:Unit|get|Flaming Phoenix|abilities.1.abilityType|ru}}
Особая способность
abilities.1.ability_tier Ability rank or tier
{{#invoke:Unit|get|Flaming Phoenix|abilities.1.ability_tier|ru}}
6
abilities.1.energyLevel Energy required for ability activation
{{#invoke:Unit|get|Flaming Phoenix|abilities.1.energyLevel|ru}}
3
abilities.1.icon Ability icon filename
{{#invoke:Unit|get|Flaming Phoenix|abilities.1.icon|ru}}
Phoenix_ability_2.png

Passive Abilities

To access specific passive ability properties, use the format: passives.index.property

Name Description Example Result
passives.1.name Passive ability name (localized)
{{#invoke:Unit|get|Giant Toad|passives.1.name}}
Melee Attack
passives.1.description Passive ability description (localized with data substitutions)
{{#invoke:Unit|get|Giant Toad|passives.1.description}}
Can only attack adjacent enemies. Provokes counterattacks.
passives.1.excaptionInTooltip Passive ability restrictions or exceptions (localized)
{{#invoke:Unit|get|Giant Toad|passives.1.excaptionInTooltip}}
passives.1.icon Passive ability icon filename
{{#invoke:Unit|get|Giant Toad|passives.1.icon}}
Base passive melee attack.png

Alternative Attacks

To access specific alternative attack properties, use the format: alternativeAttacks.index.property

Name Description Example Result
alternativeAttacks.1.name Alternative attack name (localized)
{{#invoke:Unit|get|Infiltrator|alternativeAttacks.1.name}}
Fighting Style: Hit and Run
alternativeAttacks.1.description Alternative attack description (localized with data substitutions)
{{#invoke:Unit|get|Infiltrator|alternativeAttacks.1.description}}
This creature will return to its original position after attacking, but deal –50% Damage.
alternativeAttacks.1.abilityType Type of alternative attack (localized)
{{#invoke:Unit|get|Infiltrator|alternativeAttacks.1.abilityType}}
Alternative Attack
alternativeAttacks.1.infoDescription Additional alternative attack information (localized)
{{#invoke:Unit|get|Infiltrator|alternativeAttacks.1.infoDescription}}
Does not spend Focus Charges.
alternativeAttacks.1.icon Alternative attack icon filename
{{#invoke:Unit|get|Infiltrator|alternativeAttacks.1.icon}}
Assassin_ability_1.png
alternativeAttacks.1.ability_tier Alternative attack rank or tier
{{#invoke:Unit|get|Infiltrator|alternativeAttacks.1.ability_tier}}
1

Localization Support

The module supports localization in multiple languages. You can specify the language as an optional third parameter:

{{#invoke:Unit|get|Unit Name|Property Name|Language Code}}

For example:

  • {{#invoke:Unit|get|Angel|name|pl}}
    Anioł - Polish
  • {{#invoke:Unit|get|Angel|name|ru}}
    Ангел - Russian

local Unit = {}

-- Cache for data
local dataCache = nil
-- Cache for reverse lookup: id -> unitName
local idIndexCache = nil

-- Load localization module
local LangStrings = require("Module:LocGetString")

-- Roman numeral conversion (1–20 should cover all tiers)
local function toRoman(n)
    n = tonumber(n)
    if not n or n <= 0 then return tostring(n or "") end
    local vals   = {1000,900,500,400,100,90,50,40,10,9,5,4,1}
    local syms   = {"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"}
    local result = ""
    for i, v in ipairs(vals) do
        while n >= v do
            result = result .. syms[i]
            n = n - v
        end
    end
    return result
end

-- Fallback display names used when localization returns empty.
-- Для neutral локализации нет — задаём вручную для всех языков.
local FACTION_DISPLAY = {
    demon    = "Hive",
    dungeon  = "Dungeon",
    human    = "Temple",
    nature   = "Grove",
    undead   = "Necropolis",
    unfrozen = "Schism",
}

local NEUTRAL_BY_LANG = {
    en = "Neutral",
    ru = "Нейтральные",
    pl = "Neutralne",
    fr = "Neutre",
}

-- Function to load JSON data from wiki page
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

-- Load unit data from unitdata_work.json
local function loadUnits()
    if dataCache then return dataCache end
    dataCache = {}
    idIndexCache = {}

    local units = loadJson("Data:units.json")

    for name, unit in pairs(units) do
        dataCache[name] = unit
        -- Build reverse index: id -> display name (key in the JSON object)
        if unit.id and unit.id ~= "" then
            idIndexCache[unit.id] = name
        end
    end

    return dataCache
end

-- Return the reverse-index cache (loads units first if needed)
local function getIdIndex()
    if not idIndexCache then loadUnits() end
    return idIndexCache
end

-- Function to get localized text
local function getLocalizedText(sid, lang, returnEmptyIfMissing)
    if sid and sid ~= "" then
        local result = LangStrings.getText({args = {sid = sid, lang = lang}})
        return result
    else
        if returnEmptyIfMissing then
            return ""
        else
            return sid
        end
    end
end

-- Function to get localized description with data substitutions
local function getLocalizedDescription(item, lang)
    if not item or not item.sid_desc then
        return ""
    end

    local text = LangStrings.getText({ args = { sid = item.sid_desc, lang = lang } }) or ""
    local data = item.data or {}

    local replacements = {}

    if item.data_sid and type(item.data_sid) == "table" then
        for i, sid in ipairs(item.data_sid) do
            if sid and sid ~= "" then
                local part = LangStrings.getText({ args = { sid = sid, lang = lang } }) or ""
                if part ~= "" then
                    if type(data[i]) == "table" then
                        part = part:gsub("{(%d+)}", function(n)
                            local idx = tonumber(n) + 1
                            local val = data[i][idx]
                            return val ~= nil and tostring(val) or "{" .. n .. "}"
                        end)
                        replacements[i] = part
                    else
                        part = part:gsub("{0}", tostring(data[i]))
                        replacements[i] = part
                    end
                end
            elseif data[i] ~= nil then
                replacements[i] = tostring(data[i])
            end
        end

        for i = #item.data_sid + 1, #data do
            if data[i] ~= nil then
                replacements[i] = tostring(data[i])
            end
        end
    else
        for i, val in ipairs(data) do
            replacements[i] = tostring(val)
        end
    end

    text = text:gsub("{(%d+)}", function(n)
        local idx = tonumber(n) + 1
        local val = replacements[idx]
        return val or "{" .. n .. "}"
    end)

    return text
end

-- Return the display name of a unit (with localization)
local function getUnitName(unit, lang)
    if unit.sid_name and unit.sid_name ~= "" then
        return getLocalizedText(unit.sid_name, lang, false)
    end
    local id = unit.id or ""
    if id ~= "" then
        return getLocalizedText(id .. "_name", lang, false)
    end
    return unit.name or "No name"
end

-- Return the display name of an upgrade/base unit by its id string.
-- Returns "" if the id is nil, empty, or no matching unit exists.
local function getNameById(id, lang)
    if not id or id == "" then return "" end
    local idx = getIdIndex()
    local unitName = idx[id]
    if not unitName then return "" end
    local units = loadUnits()
    local unit = units[unitName]
    if not unit then return "" end
    local displayName = getUnitName(unit, lang)
    -- Remove apostrophes, replace spaces with nothing
    displayName = displayName:gsub("’", "'")
    return displayName
end

-- Helper: get unitCost property
local function getUnitCostProperty(unitCost, propName)
    if unitCost == nil or type(unitCost) ~= "table" then return "" end
    local value = unitCost[propName]
    return value ~= nil and tostring(value) or ""
end

-- Helper: format a cost table as wiki markup
local function formatCost(unitCost)
    if unitCost == nil or type(unitCost) ~= "table" then return "" end

    local result = {}

    if unitCost.gold and type(unitCost.gold) == "number" and unitCost.gold > 0 then
        table.insert(result, tostring(unitCost.gold) ..
            "[[File:Icon Resource Gold.png|32px|link=Resources#Gold|Gold]]")
    end

    for resName, resValue in pairs(unitCost) do
        if resName ~= "gold" and type(resValue) == "number" and resValue > 0 then
            local resNameUpper = string.upper(string.sub(resName, 1, 1)) .. string.sub(resName, 2)
            table.insert(result, tostring(resValue) ..
                "[[File:Icon Resource " .. resNameUpper .. ".png|32px|link=Resources#" ..
                resNameUpper .. "|" .. resNameUpper .. "]]")
        end
    end

    return table.concat(result, " ")
end

-- Helper: process a sub-object (array element or baseClass) for a property request
local function getSubObjectProperty(element, propName, lang)
    if element == nil then return "" end
    if type(element) ~= "table" then return "" end

    -- 'name' – prefer sid_name
    if propName == "name" then
        if element["sid_name"] then
            return getLocalizedText(element["sid_name"], lang, false)
        elseif element["name"] then
            return tostring(element["name"])
        else
            return ""
        end
    end

    -- 'description' – use sid_desc with data substitution
    if propName == "description" then
        if element["sid_desc"] then
            return getLocalizedDescription(element, lang)
        else
            return ""
        end
    end

    local value = element[propName]
    if value ~= nil then
        -- Localized fields that should return empty when the sid is missing
        if propName == "abilityType" or propName == "excaptionInTooltip" or propName == "infoDescription" then
            return getLocalizedText(value, lang, true)
        else
            return tostring(value)
        end
    end

    return ""
end

-- ============================================================
-- Main exported function
-- ============================================================
function Unit.get(frame)
    local args = frame.args
    local unitName = mw.text.trim(args[1] or args.unit or "")
    local property = mw.text.trim(args[2] or args.property or "")
    local lang     = mw.text.trim(args[3] or args.lang or "en")

    if unitName == "" then return "Unit name not specified" end
    if property == "" then return "Property not specified" end

    local units = loadUnits()
    local unit  = units[unitName]
    if not unit then return "Unit '" .. unitName .. "' not found" end

    -- --------------------------------------------------------
    -- tier  →  Roman numerals
    -- --------------------------------------------------------
    if property == "tier" then
        local t = unit["tier"]
        if t == nil and unit.stats then t = unit.stats["tier"] end
        return t ~= nil and toRoman(t) or ""
    end

    -- --------------------------------------------------------
    -- name
    -- --------------------------------------------------------
    if property == "name" then
        local displayName = getUnitName(unit, lang)
        -- Remove apostrophes, replace spaces with nothing
        displayName = displayName:gsub("’", "'")
        return displayName
    end

    -- --------------------------------------------------------
    -- faction  →  localized faction name
    -- --------------------------------------------------------
    if property == "faction" then
        local factionKey = unit["faction"]
        if not factionKey or factionKey == "" then return "" end
        -- neutral отсутствует в локализации — возвращаем хардкод по языку
        if factionKey == "neutral" then
            return NEUTRAL_BY_LANG[lang] or NEUTRAL_BY_LANG["en"]
        end
        local loc = getLocalizedText("world_cheat_dropdown_fraction_" .. factionKey, lang, true)
        if loc and loc ~= "" then return loc end
        return FACTION_DISPLAY[factionKey] or factionKey
    end

    -- --------------------------------------------------------
    -- Вспомогательная: получить базовый id цепочки.
    -- phoenix_upg_alt -> phoenix
    -- phoenix_upg     -> phoenix
    -- phoenix         -> phoenix
    -- --------------------------------------------------------
    local function getBaseId()
        local myId = unit["id"] or ""
        -- Сначала пробуем убрать длинный суффикс, потом короткий
        local base = myId:match("^(.+)_upg_alt$") or myId:match("^(.+)_upg$") or myId
        return base
    end

    -- --------------------------------------------------------
    -- baseName  ->  имя базового существа цепочки (всегда одно и то же
    --               для базы, грейда и альтгрейда)
    -- --------------------------------------------------------
    if property == "baseName" then
        return getNameById(getBaseId(), lang)
    end

    -- --------------------------------------------------------
    -- upgradeName  ->  имя первого апгрейда (baseId_upg)
    --                  одинаково для любого юнита цепочки
    -- --------------------------------------------------------
    if property == "upgradeName" then
        return getNameById(getBaseId() .. "_upg", lang)
    end

    -- --------------------------------------------------------
    -- altUpgradeName  ->  имя альт-апгрейда (baseId_upg_alt)
    --                     одинаково для любого юнита цепочки
    -- --------------------------------------------------------
    if property == "altUpgradeName" then
        return getNameById(getBaseId() .. "_upg_alt", lang)
    end

    -- --------------------------------------------------------
    -- upgradeCost / upgradeCost.propertyName
    -- Разница в стоимости между апгрейднутым существом и базовым.
    -- Одинакова для обоих апгрейдов (alt и обычного), т.к. оба
    -- стоят одинаково, а базовый юнит — тот, чей upgradeSid == id текущего.
    -- Для базового существа (у которого нет предшественника) — пустая строка.
    -- --------------------------------------------------------

    -- Найти базовый юнит через стрипинг суффикса id (та же логика что getBaseId)
    local function findBaseUnit()
        local myId = unit["id"] or ""
        if myId == "" then return nil end
        local baseId = myId:match("^(.+)_upg_alt$") or myId:match("^(.+)_upg$")
        if not baseId then return nil end  -- уже базовый, нет предшественника
        local idx = getIdIndex()
        local baseName = idx[baseId]
        if not baseName then return nil end
        return units[baseName]
    end

    -- Вычислить разницу unitCost: текущий минус базовый
    -- Возвращает таблицу {resource -> delta} только с положительными значениями
    local function calcUpgradeCostDiff()
        local baseUnit = findBaseUnit()
        if not baseUnit then return nil end
        local myCost   = unit["unitCost"]   or {}
        local baseCost = baseUnit["unitCost"] or {}
        local diff = {}
        local allKeys = {}
        for k in pairs(myCost)   do allKeys[k] = true end
        for k in pairs(baseCost) do allKeys[k] = true end
        for k in pairs(allKeys) do
            local delta = (myCost[k] or 0) - (baseCost[k] or 0)
            if delta > 0 then
                diff[k] = delta
            end
        end
        return diff
    end

    if property == "upgradeCost" then
        local diff = calcUpgradeCostDiff()
        if not diff then return "" end
        return formatCost(diff)
    end

    local upgradeCostAccess = string.match(property, "^upgradeCost%.(.+)$")
    if upgradeCostAccess then
        local diff = calcUpgradeCostDiff()
        if not diff then return "" end
        local value = diff[upgradeCostAccess]
        return value ~= nil and tostring(value) or ""
    end

    -- --------------------------------------------------------
    -- imageName  →  unit display name without spaces and apostrophes,
    --               suitable for wiki file names
    -- --------------------------------------------------------
    if property == "imageName" then
        local displayName = getUnitName(unit, lang)
        -- Remove apostrophes, replace spaces with nothing
        displayName = displayName:gsub("['’ ]", "")
        return displayName
    end

    -- --------------------------------------------------------
    -- cost  →  formatted hire cost
    -- --------------------------------------------------------
    if property == "cost" then
        return formatCost(unit["unitCost"])
    end

    -- cost.propertyName / unitCost.propertyName
    local costAccess = string.match(property, "^cost%.(.+)$")
    if costAccess then return getUnitCostProperty(unit["unitCost"], costAccess) end

    local unitCostAccess = string.match(property, "^unitCost%.(.+)$")
    if unitCostAccess then return getUnitCostProperty(unit["unitCost"], unitCostAccess) end

    -- --------------------------------------------------------
    -- arrayName.index.propertyName  (e.g. abilities.1.name)
    -- --------------------------------------------------------
    local arrayName, indexStr, propName = string.match(property, "^(.+)%.(%d+)%.(.+)$")
    if arrayName and indexStr and propName then
        local index = tonumber(indexStr)
        local array = unit[arrayName]
        if array == nil or type(array) ~= "table" then return "" end
        local element = array[index]
        if element == nil then return "" end
        return getSubObjectProperty(element, propName, lang)
    end

    -- --------------------------------------------------------
    -- baseClass.propertyName
    -- --------------------------------------------------------
    local baseClassAccess = string.match(property, "^baseClass%.(.+)$")
    if baseClassAccess then
        local baseClass = unit["baseClass"]
        if baseClass == nil or type(baseClass) ~= "table" then return "" end
        return getSubObjectProperty(baseClass, baseClassAccess, lang)
    end

    -- --------------------------------------------------------
    -- Direct property on unit object
    -- --------------------------------------------------------
    if unit[property] ~= nil then
        if property == "name" and unit["sid_name"] then
            return getLocalizedText(unit["sid_name"], lang, false)
        elseif property == "description" and unit["sid_desc"] then
            return getLocalizedDescription(unit, lang)
        else
            return tostring(unit[property])
        end
    end

    -- description via id
    if property == "description" then
        local id = unit.id or ""
        if id ~= "" then
            return getLocalizedText(id .. "_narrativeDescription", lang, true)
        end
        return ""
    end

    -- Fallback: check stats sub-table
    if unit.stats and unit.stats[property] ~= nil then
        return tostring(unit.stats[property])
    end

    return ""
end

return Unit