Module:Unit: Difference between revisions

From Heroes of Might and Magic: Olden Era Official Wiki
No edit summary
Tags: Manual revert Reverted
No edit summary
 
(39 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


-- Function to get localized text
-- Function to get localized text
local function getLocalizedText(sid, lang)
local function getLocalizedText(sid, lang, returnEmptyIfMissing)
     if sid and sid ~= "" then
     if sid and sid ~= "" then
         return LangStrings.getText({args = {sid = sid, lang = lang}})
         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
     else
         return sid  -- Return the original value if no sid
         for i, val in ipairs(data) do
            replacements[i] = tostring(val)
        end
     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
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)
         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
end


-- Function to get unit property
-- Helper: format a cost table as wiki markup
function Unit.get(frame)
local function formatCost(unitCost)
     local args = frame.args
     if unitCost == nil or type(unitCost) ~= "table" then return "" end
    local unitName = args[1] or args.unit or ""
 
    local property = args[2] or args.property or ""
     local result = {}
     local lang = args[3] or args.lang or "en"  -- Get language parameter, default to 'en'


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


     if unitName == "" then
     for resName, resValue in pairs(unitCost) do
        return "Unit name not specified"
        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
     end


     if property == "" then
    return table.concat(result, " ")
         return "Property not specified"
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
     end


     -- Load unit data
     -- '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 units = loadUnits()
    local unit  = units[unitName]
    if not unit then return "Unit '" .. unitName .. "' not found" end


     -- Check if unit exists in data
     -- --------------------------------------------------------
    local unit = units[unitName]
    -- tier  →  Roman numerals
    if not unit then
    -- --------------------------------------------------------
         return "Unit '" .. unitName .. "' not found"
    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 is 'name', return localized name
     -- --------------------------------------------------------
    -- name
    -- --------------------------------------------------------
     if property == "name" then
     if property == "name" then
         return getUnitName(unit, lang)
         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
     end


     -- Check if property is in format "arrayName.index.propertyName" (for accessing arrays)
     -- --------------------------------------------------------
     local arrayAccess = string.match(property, "^(.+)%.(%d+)%.(.+)$")
    -- baseName  ->  имя базового существа цепочки (всегда одно и то же
     if arrayAccess then
     --              для базы, грейда и альтгрейда)
        local arrayName, indexStr, propName = string.match(property, "^(.+)%.(%d+)%.(.+)$")
    -- --------------------------------------------------------
         local index = tonumber(indexStr)
     if property == "baseName" then
        if arrayName and index and propName then
         return getNameById(getBaseId(), lang)
            -- Convert from 1-based indexing (user-friendly) to 0-based indexing (JSON format)
    end
            local actualIndex = index - 1
            local array = unit[arrayName]


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


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


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


            -- If element is not found at the converted index, it might mean that the array uses 1-based indexing
    -- Найти базовый юнит через стрипинг суффикса id (та же логика что getBaseId)
            -- This can happen depending on how the JSON was parsed
    local function findBaseUnit()
            if element == nil then
        local myId = unit["id"] or ""
                -- Try with 1-based indexing (standard Lua arrays)
        if myId == "" then return nil end
                element = array[index]
        local baseId = myId:match("^(.+)_upg_alt$") or myId:match("^(.+)_upg$")
                if element ~= nil then
        if not baseId then return nil end  -- уже базовый, нет предшественника
                    -- Use 1-based indexing, so we don't subtract 1
        local idx = getIdIndex()
                    actualIndex = index
        local baseName = idx[baseId]
                end
        if not baseName then return nil end
            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()
                if value ~= nil then
        local baseUnit = findBaseUnit()
                    -- If this property has localization (for example, name fields), return localized version
        if not baseUnit then return nil end
                    if propName == "name" and element["sid_name"] then
        local myCost  = unit["unitCost"]   or {}
                        return getLocalizedText(element["sid_name"], lang)
        local baseCost = baseUnit["unitCost"] or {}
                    else
        local diff = {}
                        return tostring(value)
        local allKeys = {}
                    end
        for k in pairs(myCost)   do allKeys[k] = true end
                else
        for k in pairs(baseCost) do allKeys[k] = true end
                    return "Property '" .. propName .. "' not found in " .. arrayName .. "[" .. actualIndex .. "] for unit '" .. unitName .. "'"
        for k in pairs(allKeys) do
                end
            local delta = (myCost[k] or 0) - (baseCost[k] or 0)
             else
             if delta > 0 then
                 return "Array '" .. arrayName .. "' does not contain index " .. actualIndex .. " for unit '" .. unitName .. "' (requested as " .. index .. " for user-friendly indexing)"
                 diff[k] = delta
             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
     end


     -- First check if property exists in main unit object
     -- --------------------------------------------------------
    -- 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 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