Module:Unit: Difference between revisions

From Heroes of Might and Magic: Olden Era Official Wiki
No edit summary
No edit summary
 
(31 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


     return dataCache
     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
end


Line 42: Line 87:
             return ""
             return ""
         else
         else
             return sid  -- Return the original value if no sid
             return sid
         end
         end
     end
     end
Line 58: Line 103:
     local replacements = {}
     local replacements = {}


    -- If there are data_sid, localize each element and substitute data
     if item.data_sid and type(item.data_sid) == "table" then
     if item.data_sid and type(item.data_sid) == "table" then
         for i, sid in ipairs(item.data_sid) do
         for i, sid in ipairs(item.data_sid) do
Line 64: Line 108:
                 local part = LangStrings.getText({ args = { sid = sid, lang = lang } }) or ""
                 local part = LangStrings.getText({ args = { sid = sid, lang = lang } }) or ""
                 if part ~= "" then
                 if part ~= "" then
                    -- If data[i] is a table (array of values)
                     if type(data[i]) == "table" then
                     if type(data[i]) == "table" then
                        -- Replace placeholders {0}, {1}, {2}, etc. with data from data[i]
                         part = part:gsub("{(%d+)}", function(n)
                         part = part:gsub("{(%d+)}", function(n)
                             local idx = tonumber(n) + 1  -- +1 because Lua indices start with 1
                             local idx = tonumber(n) + 1
                             local val = data[i][idx]
                             local val = data[i][idx]
                             return val ~= nil and tostring(val) or "{" .. n .. "}"
                             return val ~= nil and tostring(val) or "{" .. n .. "}"
Line 74: Line 116:
                         replacements[i] = part
                         replacements[i] = part
                     else
                     else
                        -- If data[i] is a single value, replace only {0}
                         part = part:gsub("{0}", tostring(data[i]))
                         part = part:gsub("{0}", tostring(data[i]))
                         replacements[i] = part
                         replacements[i] = part
Line 84: Line 125:
         end
         end


        -- Add remaining elements from data that are not in data_sid
         for i = #item.data_sid + 1, #data do
         for i = #item.data_sid + 1, #data do
             if data[i] ~= nil then
             if data[i] ~= nil then
Line 91: Line 131:
         end
         end
     else
     else
        -- If no data_sid, use data directly
         for i, val in ipairs(data) do
         for i, val in ipairs(data) do
             replacements[i] = tostring(val)
             replacements[i] = tostring(val)
Line 97: Line 136:
     end
     end


    -- Substitute replacements in the base text
     text = text:gsub("{(%d+)}", function(n)
     text = text:gsub("{(%d+)}", function(n)
         local idx = tonumber(n) + 1
         local idx = tonumber(n) + 1
Line 107: Line 145:
end
end


-- Function to get unit name using localization
-- Return the display name of a unit (with localization)
local function getUnitName(unit, lang)
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 ""
     local id = unit.id or ""
     if id ~= "" then
     if id ~= "" then
         return getLocalizedText(id .. "_name", lang, false)
         return getLocalizedText(id .. "_name", lang, false)
    else
        return unit.name or "No name"
     end
     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
end


-- Function to get unit property
-- ============================================================
-- 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


     unitName = mw.text.trim(unitName or "")
     local units = loadUnits()
     property = mw.text.trim(property or "")
     local unit  = units[unitName]
    lang = mw.text.trim(lang or "en")
    if not unit then return "Unit '" .. unitName .. "' not found" end


     if unitName == "" then
    -- --------------------------------------------------------
         return "Unit name not specified"
    -- 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
     end


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


     -- Load unit data
     -- --------------------------------------------------------
    local units = loadUnits()
    -- 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


     -- Check if unit exists in data
     -- --------------------------------------------------------
     local unit = units[unitName]
    -- Вспомогательная: получить базовый id цепочки.
    if not unit then
    -- phoenix_upg_alt -> phoenix
         return "Unit '" .. unitName .. "' not found"
    -- 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
     end


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


     -- Check if property is in format "arrayName.index.propertyName" (for accessing arrays)
     -- --------------------------------------------------------
     local arrayAccess = string.match(property, "^(.+)%.(%d+)%.(.+)$")
    -- upgradeName  ->  имя первого апгрейда (baseId_upg)
    if arrayAccess then
     --                  одинаково для любого юнита цепочки
         local arrayName, indexStr, propName = string.match(property, "^(.+)%.(%d+)%.(.+)$")
    -- --------------------------------------------------------
        local index = tonumber(indexStr)
    if property == "upgradeName" then
        if arrayName and index and propName then
         return getNameById(getBaseId() .. "_upg", lang)
            local array = unit[arrayName]
    end


            -- Check if the array exists and is a table
    -- --------------------------------------------------------
            if array == nil then
    -- altUpgradeName  ->  имя альт-апгрейда (baseId_upg_alt)
                return "Array '" .. arrayName .. "' not found for unit '" .. unitName .. "'"
    --                    одинаково для любого юнита цепочки
            end
    -- --------------------------------------------------------
    if property == "altUpgradeName" then
        return getNameById(getBaseId() .. "_upg_alt", lang)
    end


            if type(array) ~= "table" then
    -- --------------------------------------------------------
                return "Property '" .. arrayName .. "' is not an array for unit '" .. unitName .. "'"
    -- upgradeCost / upgradeCost.propertyName
            end
    -- Разница в стоимости между апгрейднутым существом и базовым.
    -- Одинакова для обоих апгрейдов (alt и обычного), т.к. оба
    -- стоят одинаково, а базовый юнит — тот, чей upgradeSid == id текущего.
    -- Для базового существа (у которого нет предшественника) — пустая строка.
    -- --------------------------------------------------------


            -- Use 1-based indexing as per Lua convention
    -- Найти базовый юнит через стрипинг суффикса id (та же логика что getBaseId)
            local element = array[index]
    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


            -- Now check if we found an element
    -- Вычислить разницу unitCost: текущий минус базовый
            if element ~= nil and type(element) == "table" then
    -- Возвращает таблицу {resource -> delta} только с положительными значениями
                local value = element[propName]
    local function calcUpgradeCostDiff()
                -- Special handling for 'name' property - use localization if sid_name exists (higher priority)
        local baseUnit = findBaseUnit()
                if propName == "name" and element["sid_name"] then
        if not baseUnit then return nil end
                    return getLocalizedText(element["sid_name"], lang, false)
        local myCost  = unit["unitCost"]   or {}
                elseif value ~= nil then
        local baseCost = baseUnit["unitCost"] or {}
                    -- Handle special localized fields that should return empty string if missing
        local diff = {}
                    if propName == "abilityType" or propName == "excaptionInTooltip" or propName == "infoDescription" then
        local allKeys = {}
                        return getLocalizedText(value, lang, true)
        for k in pairs(myCost)  do allKeys[k] = true end
                    else
        for k in pairs(baseCost) do allKeys[k] = true end
                        return tostring(value)
        for k in pairs(allKeys) do
                    end
            local delta = (myCost[k] or 0) - (baseCost[k] or 0)
                else
             if delta > 0 then
                    -- If property is 'description', return localized description with data substitutions
                 diff[k] = delta
                    if propName == "description" and element["sid_desc"] then
                        return getLocalizedDescription(element, lang)
                    else
                        return "Property '" .. propName .. "' not found in " .. arrayName .. "[" .. index .. "] for unit '" .. unitName .. "'"
                    end
                end
             else
                 return "Array '" .. arrayName .. "' does not contain index " .. index .. " for unit '" .. unitName .. "'"
             end
             end
         end
         end
        return diff
     end
     end


     -- Check if property is in format "baseClass.propertyName" (for accessing baseClass object)
     if property == "upgradeCost" then
     local baseClassAccess = string.match(property, "^baseClass%.(.+)$")
        local diff = calcUpgradeCostDiff()
     if baseClassAccess then
        if not diff then return "" end
         local baseClass = unit["baseClass"]
        return formatCost(diff)
         if baseClass == nil then
    end
            return "baseClass not found for unit '" .. unitName .. "'"
 
        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


        if type(baseClass) ~= "table" then
    -- --------------------------------------------------------
            return "baseClass is not an object for unit '" .. unitName .. "'"
    -- imageName  →  unit display name without spaces and apostrophes,
         end
    --              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


        local propName = baseClassAccess
    -- --------------------------------------------------------
        local value = baseClass[propName]
    -- cost  →  formatted hire cost
        -- Special handling for 'name' property - use localization if sid_name exists (higher priority)
    -- --------------------------------------------------------
        if propName == "name" and baseClass["sid_name"] then
    if property == "cost" then
            return getLocalizedText(baseClass["sid_name"], lang, false)
        return formatCost(unit["unitCost"])
        elseif value ~= nil then
            -- Handle special localized fields that should return empty string if missing
            if propName == "abilityType" or propName == "excaptionInTooltip" or propName == "infoDescription" then
                return getLocalizedText(value, lang, true)
            else
                return tostring(value)
            end
        else
            -- If property is 'description', return localized description with data substitutions
            if propName == "description" and baseClass["sid_desc"] then
                return getLocalizedDescription(baseClass, lang)
            else
                return "Property '" .. propName .. "' not found in baseClass for unit '" .. unitName .. "'"
            end
        end
     end
     end


     -- Check if property is in format "cost.propertyName" (for accessing unitCost object)
     -- cost.propertyName / unitCost.propertyName
     local costAccess = string.match(property, "^cost%.(.+)$")
     local costAccess = string.match(property, "^cost%.(.+)$")
     if costAccess then
     if costAccess then return getUnitCostProperty(unit["unitCost"], costAccess) end
        local unitCost = unit["unitCost"]
        if unitCost == nil then
            return "unitCost not found for unit '" .. unitName .. "'"
        end


        if type(unitCost) ~= "table" then
    local unitCostAccess = string.match(property, "^unitCost%.(.+)$")
            return "unitCost is not an object for unit '" .. unitName .. "'"
    if unitCostAccess then return getUnitCostProperty(unit["unitCost"], unitCostAccess) end
        end


         local propName = costAccess
    -- --------------------------------------------------------
         local value = unitCost[propName]
    -- 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


        if value ~= nil then
    -- --------------------------------------------------------
            return tostring(value)
    -- baseClass.propertyName
         else
    -- --------------------------------------------------------
            return "Property '" .. propName .. "' not found in unitCost for unit '" .. unitName .. "'"
    local baseClassAccess = string.match(property, "^baseClass%.(.+)$")
         end
    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
        -- Special handling for 'name' property - use localization if sid_name exists (higher priority)
         if property == "name" and unit["sid_name"] then
         if property == "name" and unit["sid_name"] then
             return getLocalizedText(unit["sid_name"], lang, false)
             return getLocalizedText(unit["sid_name"], lang, false)
        elseif property == "description" and unit["sid_desc"] then
            return getLocalizedDescription(unit, lang)
         else
         else
             return tostring(unit[property])
             return tostring(unit[property])
Line 263: Line 443:
     end
     end


     -- Then check if property exists in stats
     -- 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
     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