Module:Unit

From Heroes of Might and Magic: Olden Era Official Wiki

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",
}

-- 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
    return getUnitName(unit, lang)
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