Module:HeroClass

From Heroes of Might and Magic: Olden Era Official Wiki
Revision as of 08:28, 30 May 2026 by akesha.taladim (talk | contribs) (Created page with "local p = {} -- ========================================================================= -- CONFIGURATION & TABLES DE CORRESPONDANCE -- ========================================================================= -- Associe les noms de classes saisis par les utilisateurs aux clés réelles du fichier JSON local class_mapping = { ["herald"] = "magic_demon", ["warlock"] = "magic_dungeon", ["cleric"] = "magic_human", ["druid"] = "magic_na...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Description

This module is a comprehensive utility for generating hero class profiles. It integrates statistical data, secondary skill probabilities, and subclass information into a unified, styled infobox layout. It handles multi-language support and automatic data linking via Cargo.

Usage

You can invoke specific sections or the entire main box:

{{#invoke:HeroList|getMainBox|class=...|language=...}}
{{#invoke:HeroList|getStatsTable|class=...}}
{{#invoke:HeroList|getSkillsTable|class=...}}
{{#invoke:HeroList|getSubclassesBlock|class=...}}

Parameters

Parameter Description Example
class The identifier or common name of the hero class (e.g., knight, herald, warlock). class=Knight
language Forces the Wiki language (if omitted, it detects the page suffix). language=fr

Key Features

  • Main Infobox (getMainBox): Combines class crests, base stats, skill probabilities, and subclasses into a cohesive, responsive container.
  • Dynamic Statistics (getStatsTable): Displays base hero attributes along with growth rates at levels 1-12 and 13+. Features automatic icon rendering.
  • Skill Probabilities (getSkillsTable): Retrieves "Skill Roll Weights" from Cargo to generate a comparison table showing the likelihood of learning specific secondary skills.
  • Subclass Integration (getSubclassesBlock): Dynamically fetches subclass data, including localization for names and descriptions, and lists activation skills with icons.
  • Smart Localization: Automatically detects page language (e.g., /fr) and falls back to English if translations are missing in the Translation table.

Implementation Example

To display the full infobox for the "Knight" class:

{{#invoke:HeroList|getMainBox|class=Knight}}

Technical Notes

  • The module uses several Cargo tables: HeroClass, SkillRollWeight, HeroSubClass, and Translation.
  • The class_mapping table within the script translates user-friendly names (e.g., "knight") into technical Cargo IDs (e.g., "might_human").
  • CSS is embedded inline to ensure the infobox maintains a consistent look across different Wiki skins, using a dark-theme style (#1a1f26 background).

local p = {}

-- =========================================================================
-- CONFIGURATION & TABLES DE CORRESPONDANCE
-- =========================================================================

-- Associe les noms de classes saisis par les utilisateurs aux clés réelles du fichier JSON
local class_mapping = {
    ["herald"]      = "magic_demon",
    ["warlock"]     = "magic_dungeon",
    ["cleric"]      = "magic_human",
    ["druid"]       = "magic_nature",
    ["necromancer"] = "magic_undead",
    ["riftspeaker"] = "magic_unfrozen",
    ["enforcer"]    = "might_demon",
    ["overlord"]    = "dungeon_might",
    ["knight"]      = "might_human", 
    ["warden"]      = "might_nature",
    ["death knight"]= "might_undead",
    ["oathkeeper"]  = "might_unfrozen"
}

-- Structure exacte de tes tables Cargo validée par tes soins (Matrice 12x2 complète)
local skills_structure = {
    { {"Offence", "offenceSkill", "skill_assault"}, {"Defence", "defenceSkill", "skill_protection"} },
    { {"Resistance", "resistanceSkill", "skill_resistance"}, {"Battlecraft", "battlecraftSkill", "skill_formation"} },
    { {"Sorcery", "sorcerySkill", "skill_sorcery"}, {"Intelligence", "intelligenceSkill", "skill_mastery"} },
    { {"Summon Avatar", "summonAvatarSkill", "skill_summoner"}, {"Battle Magic", "battleMagicSkill", "skill_battlemage"} },
    { {"Daylight Magic", "daylightSkill", "skill_magic_day"}, {"Nightshade Magic", "nightshadeSkill", "skill_magic_night"} },
    { {"Arcane Magic", "arcaneSkill", "skill_magic_space"}, {"Primal Magic", "primalSkill", "skill_magic_primal"} },
    { {"Leadership", "leadershipSkill", "skill_leadership"}, {"Luck", "luckSkill", "skill_luck"} },
    { {"Insight", "insightSkill", "skill_enlightenment"}, {"Diplomacy", "diplomacySkill", "skill_diplomacy"} },
    { {"Logistics", "logisticsSkill", "skill_logistic"}, {"Scouting", "scoutingSkill", "skill_scouting"} },
    { {"Economy", "economySkill", "skill_economy"}, {"Tactics", "tacticsSkill", "skill_tactics"} },
    { {"Siegecraft", "siegecraftSkill", "skill_siege"}, {"Recruitment", "recruitmentSkill", "skill_trainer"} },
    { {"Thaumaturgy", "thaumaturgySkill", "skill_wisdom"}, {"Combat", "battleartistrySkill", "skill_battle_artistry"} }
}

-- =========================================================================
-- FONCTIONS UTILITAIRES INTERNES
-- =========================================================================

-- Détecte automatiquement la langue via le titre (ex: "Chevalier/fr" -> "fr", "/fr")
local function getLanguageInfo()
    local currentTitle = mw.title.getCurrentTitle().text
    local lang_suffix = string.match(currentTitle, "/([a-z][a-z])$")
    
    if lang_suffix then
        return lang_suffix, "/" .. lang_suffix
    else
        return "en", "" -- Pas de suffixe = Anglais
    end
end

-- Charge et met en cache les données du fichier JSON de la langue ciblée
local json_cache = {}
local function getHeroClassJson(lang)
    if not json_cache[lang] then
        local page_name = string.format('Data:HeroClassData/%s.json', lang)
        local content = mw.title.new(page_name):getContent() or '{}'
        json_cache[lang] = mw.text.jsonDecode(content)
    end
    return json_cache[lang]
end

-- Trouve l'objet de la classe de héros demandée dans le JSON
local function findHeroClassData(className, lang)
    if not className then return nil end
    local json = getHeroClassJson(lang)
    if not json or not json.heroClasses then return nil end
    
    local clean_name = mw.text.trim(string.lower(className))
    local target_key = class_mapping[clean_name] or clean_name
    
    for _, class_obj in ipairs(json.heroClasses) do
        local current_class_name = string.lower(class_obj.name or "")
        if current_class_name == target_key or current_class_name == clean_name then
            return class_obj
        end
    end
    return nil
end

-- Interroge Cargo pour récupérer un dictionnaire global [id_competence] = "Nom Traduit"
local function fetchTranslationsFromCargo(lang)
    local translations = {}
    local tables = "Skill, Translation"
    local fields = "Skill.id=id, Translation.name=name"
    
    local query_opts = {
        join = "Skill.id = Translation.target_id",
        where = string.format("Translation.type = 'skill' AND Translation.language = '%s'", lang),
        limit = 300
    }
    
    local res = mw.ext.cargo.query(tables, fields, query_opts)
    if res then
        for _, row in ipairs(res) do
            if row.id and row.name then
                translations[string.lower(mw.text.trim(row.id))] = mw.text.trim(row.name)
            end
        end
    end
    
    -- Complétion en anglais pour les trous
    if lang ~= "en" then
        query_opts.where = "Translation.type = 'skill' AND Translation.language = 'en'"
        local backup_res = mw.ext.cargo.query(tables, fields, query_opts)
        if backup_res then
            for _, row in ipairs(backup_res) do
                local id_clean = string.lower(mw.text.trim(row.id or ""))
                if id_clean ~= "" and not translations[id_clean] then
                    translations[id_clean] = mw.text.trim(row.name or "")
                end
            end
        end
    end
    
    return translations
end

-- Formate proprement les valeurs numériques en pourcentages
local function formatPercentage(val)
    if not val or val == "" or val == 0 or val == "0" then return "0%" end
    if type(val) == "string" and string.match(val, "%%$") then return val end
    return tostring(val) .. "%"
end

-- =========================================================================
-- FONCTIONS PUBLIQUES
-- =========================================================================

-- 1. Génère le grand tableau des compétences secondaires (Maintenant 12 lignes complètes)
function p.getSkillsTable(frame)
    local args = frame.args[1] and frame.args or frame:getParent().args
    local lang, suffix = getLanguageInfo()
    local class_data = findHeroClassData(args.class or args[1], lang)
    
    if not class_data then 
        return "" 
    end

    local dictionary = fetchTranslationsFromCargo(lang)
    
    local html = {}
    table.insert(html, '{| class="wikitable" style="width:100%; border-collapse:collapse;"')
    
    for _, double_row in ipairs(skills_structure) do
        local s1, s2 = double_row[1], double_row[2]
        
        local label1 = dictionary[s1[3]] or s1[1]
        local label2 = dictionary[s2[3]] or s2[1]
        
        table.insert(html, '|-')
        table.insert(html, string.format('| [[File:Skill_%s.png|32px|link=%s%s]] %s || %s || [[File:Skill_%s.png|32px|link=%s%s]] %s || %s', 
            s1[1], s1[1], suffix, label1, formatPercentage(class_data[s1[2]]), 
            s2[1], s2[1], suffix, label2, formatPercentage(class_data[s2[2]])
        ))
    end
    
    table.insert(html, '|}')
    return table.concat(html, "\n")
end

-- 2. Génère le bloc visuel des sous-classes
function p.getSubclassesBlock(frame)
    local args = frame.args[1] and frame.args or frame:getParent().args
    local lang, suffix = getLanguageInfo()
    local class_data = findHeroClassData(args.class or args[1], lang)
    
    if not class_data or not class_data.subclasses then 
        return "" 
    end

    local titles = { fr = "Sous-classes", de = "Unterklassen", es = "Subclases", it = "Sottoclassi", pl = "Podklasy", en = "Subclasses" }
    local title_text = titles[lang] or titles["en"]

    local keys = {}
    for k in pairs(class_data.subclasses) do table.insert(keys, k) end
    table.sort(keys, function(a, b) return a > b end)

    local html = {}
    table.insert(html, string.format('<div style="font-weight:bold; text-align:center; margin-top:8px; margin-bottom:6px;">%s</div>', title_text))

    for _, sub_key in ipairs(keys) do
        local sub = class_data.subclasses[sub_key]
        if sub then
            table.insert(html, '<div style="margin:4px 0; border:1px solid rgba(255,255,255,0.04); border-radius:4px; padding:6px;">')
            
            local display_name = sub.name or sub_key
            table.insert(html, string.format('  <div style="text-align:center; font-weight:bold;">[[File:%s.png|60px]] %s</div>', sub_key, display_name))
            
            local effect_desc = sub.effect or sub.description or ""
            table.insert(html, string.format('  <div style="text-align:center; margin-top:4px; font-size:0.95em;">%s</div>', effect_desc))
            
            table.insert(html, '  <table style="margin:auto; margin-top:6px;">')
            table.insert(html, '    <tr>')
            
            if sub.skills then
                for i = 1, 5 do
                    local name_skill = sub.skills[i]
                    if name_skill and name_skill ~= "" then
                        table.insert(html, string.format('      <td style="padding:0 3px;">[[File:Skill_%s.png|60px|link=%s%s]]</td>', name_skill, name_skill, suffix))
                    else
                        table.insert(html, '      <td></td>')
                    end
                end
            end
            
            table.insert(html, '    </tr>')
            table.insert(html, '  </table>')
            table.insert(html, '</div>')
        end
    end

    return table.concat(html, "\n")
end

return p