Module:HeroClass: Difference between revisions

From Heroes of Might and Magic: Olden Era Official Wiki
No edit summary
No edit summary
Line 5: Line 5:
-- =========================================================================
-- =========================================================================


-- Structure exacte de ton tableau (Matrice 12x2 complète)
-- Associe le nom d'usage de la classe à son identifiant Cargo (class_id)
-- Format : { { Nom_Anglais_Par_Defaut, Nom_De_La_Colonne_Dans_HeroClass, Id_Dans_Translation }, ... }
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", -- Note : si le standard est "might_dungeon", le script s'adaptera aussi
    ["knight"]      = "might_human",  
    ["warden"]      = "might_nature",
    ["death knight"] = "might_undead",
    ["oathkeeper"]  = "might_unfrozen"
}
 
-- Ta structure de compétences (Matrice 12x2 complète)
local skills_structure = {
local skills_structure = {
     { {"Offence", "offenceSkill", "skill_assault"}, {"Defence", "defenceSkill", "skill_protection"} },
     { {"Offence", "", "skill_assault"}, {"Defence", "", "skill_protection"} },
     { {"Resistance", "resistanceSkill", "skill_resistance"}, {"Battlecraft", "battlecraftSkill", "skill_formation"} },
     { {"Resistance", "", "skill_resistance"}, {"Battlecraft", "", "skill_formation"} },
     { {"Sorcery", "sorcerySkill", "skill_sorcery"}, {"Intelligence", "intelligenceSkill", "skill_mastery"} },
     { {"Sorcery", "", "skill_sorcery"}, {"Intelligence", "", "skill_mastery"} },
     { {"Summon Avatar", "summonAvatarSkill", "skill_summoner"}, {"Battle Magic", "battleMagicSkill", "skill_battlemage"} },
     { {"Summon Avatar", "", "skill_summoner"}, {"Battle Magic", "", "skill_battlemage"} },
     { {"Daylight Magic", "daylightSkill", "skill_magic_day"}, {"Nightshade Magic", "nightshadeSkill", "skill_magic_night"} },
     { {"Daylight Magic", "", "skill_magic_day"}, {"Nightshade Magic", "", "skill_magic_night"} },
     { {"Arcane Magic", "arcaneSkill", "skill_magic_space"}, {"Primal Magic", "primalSkill", "skill_magic_primal"} },
     { {"Arcane Magic", "", "skill_magic_space"}, {"Primal Magic", "", "skill_magic_primal"} },
     { {"Leadership", "leadershipSkill", "skill_leadership"}, {"Luck", "luckSkill", "skill_luck"} },
     { {"Leadership", "", "skill_leadership"}, {"Luck", "", "skill_luck"} },
     { {"Insight", "insightSkill", "skill_enlightenment"}, {"Diplomacy", "diplomacySkill", "skill_diplomacy"} },
     { {"Insight", "", "skill_enlightenment"}, {"Diplomacy", "", "skill_diplomacy"} },
     { {"Logistics", "logisticsSkill", "skill_logistic"}, {"Scouting", "scoutingSkill", "skill_scouting"} },
     { {"Logistics", "", "skill_logistic"}, {"Scouting", "", "skill_scouting"} },
     { {"Economy", "economySkill", "skill_economy"}, {"Tactics", "tacticsSkill", "skill_tactics"} },
     { {"Economy", "", "skill_economy"}, {"Tactics", "", "skill_tactics"} },
     { {"Siegecraft", "siegecraftSkill", "skill_siege"}, {"Recruitment", "recruitmentSkill", "skill_trainer"} },
     { {"Siegecraft", "", "skill_siege"}, {"Recruitment", "", "skill_trainer"} },
     { {"Thaumaturgy", "thaumaturgySkill", "skill_wisdom"}, {"Combat", "battleartistrySkill", "skill_battle_artistry"} }
     { {"Thaumaturgy", "", "skill_wisdom"}, {"Combat", "", "skill_battle_artistry"} }
}
}


Line 34: Line 49:
         return lang_suffix, "/" .. lang_suffix
         return lang_suffix, "/" .. lang_suffix
     else
     else
         return "en", "" -- Pas de suffixe = Anglais par défaut
         return "en", ""  
     end
     end
end
end


-- 1. RÉCUPÉRATION DES STATISTIQUES (Depuis ta table Cargo HeroClass)
-- Génère automatiquement le nom de la table SQL à partir du class_id
local function fetchClassStatsFromCargo(className)
-- Exemple : "might_human" -> "humans_might_skills_table"
     if not className or className == "" then return nil end
-- Exemple : "magic_demon" -> "demons_magic_skills_table"
local function generateTableId(classId)
    local type_part, faction_part = string.match(classId, "([a-z]+)_([a-z]+)")
   
    -- Inversion pour gérer le cas particulier de "dungeon_might" si nécessaire
     if not type_part or not faction_part then
        faction_part, type_part = string.match(classId, "([a-z]+)_([a-z]+)")
    end
      
      
     local res = mw.ext.cargo.query("HeroClass", "*", {
    if faction_part and type_part then
         where = string.format("name = '%s'", className),
        -- Gestion du pluriel automatique pour les noms de factions Anglaises courantes dans tes tables
         limit = 1
        if faction_part == "human" then faction_part = "humans"
        elseif faction_part == "demon" then faction_part = "demons"
        elseif faction_part == "dungeon" then faction_part = "dungeons"
        elseif faction_part == "nature" then faction_part = "natures"
        elseif faction_part == "undead" then faction_part = "undeads"
        elseif faction_part == "unfrozen" then faction_part = "unfrozens"
        end
       
        return string.format("%s_%s_skills_table", faction_part, type_part)
    end
    return nil
end
 
-- 1. RÉCUPÉRATION DES POIDS (Depuis ta table Cargo SkillRollWeight)
local function fetchWeightsFromCargo(tableId)
    local weights = {}
    if not tableId then return weights end
   
     local res = mw.ext.cargo.query("SkillRollWeight", "skill_id, weight", {
         where = string.format("table_id = '%s' AND band_kind = 'default'", tableId),
         limit = 100
     })
     })
      
      
     if res and res[1] then
     if res then
         return res[1] -- Renvoie la ligne trouvée avec toutes les colonnes de pourcentages
         for _, row in ipairs(res) do
            if row.skill_id and row.weight then
                weights[string.lower(mw.text.trim(row.skill_id))] = row.weight
            end
        end
     end
     end
     return nil
     return weights
end
end


Line 74: Line 120:
     end
     end
      
      
     -- Secours en anglais pour combler les trous si la langue actuelle n'a pas tout
     -- Secours anglais si la traduction locale a des trous
     if lang ~= "en" then
     if lang ~= "en" then
         query_opts.where = "Translation.type = 'skill' AND Translation.language = 'en'"
         query_opts.where = "Translation.type = 'skill' AND Translation.language = 'en'"
Line 89: Line 135:
      
      
     return translations
     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
end


-- =========================================================================
-- =========================================================================
-- FONCTIONS PUBLIQUES
-- FONCTION PRINCIPALE
-- =========================================================================
-- =========================================================================


-- Génère le grand tableau des compétences secondaires
function p.getSkillsTable(frame)
function p.getSkillsTable(frame)
     local args = frame.args[1] and frame.args or frame:getParent().args
     local args = frame.args[1] and frame.args or frame:getParent().args
     local lang, suffix = getLanguageInfo()
     local lang, suffix = getLanguageInfo()
      
      
    -- On va chercher le nom de la classe (ex: Knight) fourni par le template
     local raw_class_name = string.lower(mw.text.trim(args.class or args[1] or ""))
     local target_class = args.class or args[1]
    local class_id = class_mapping[raw_class_name]
      
      
    -- Appel Cargo pour récupérer les pourcentages
     if not class_id then
    local class_data = fetchClassStatsFromCargo(target_class)
         return ""
   
     if not class_data then  
         return ""  
     end
     end


     -- Appel Cargo pour récupérer les noms traduits
    -- Génération dynamique de l'ID de la table de tirage (ex: "humans_might_skills_table")
     local dictionary = fetchTranslationsFromCargo(lang)
    local table_id = generateTableId(class_id)
 
    -- Appel Cargo 1 : Récupération des poids
    local weight_dictionary = fetchWeightsFromCargo(table_id)
   
     -- Appel Cargo 2 : Récupération des traductions
     local translation_dictionary = fetchTranslationsFromCargo(lang)
      
      
     local html = {}
     local html = {}
Line 126: Line 167:
         local s1, s2 = double_row[1], double_row[2]
         local s1, s2 = double_row[1], double_row[2]
          
          
        -- Recherche du nom traduit, sinon nom anglais par défaut
         local label1 = translation_dictionary[s1[3]] or s1[1]
         local label1 = dictionary[s1[3]] or s1[1]
         local label2 = translation_dictionary[s2[3]] or s2[1]
         local label2 = dictionary[s2[3]] or s2[1]
          
          
        -- Extraction du pourcentage depuis la colonne correspondante de HeroClass (ex: class_data.offenceSkill)
         local val1 = weight_dictionary[s1[3]] or "0"
         local percent1 = formatPercentage(class_data[s1[2]])
         local val2 = weight_dictionary[s2[3]] or "0"
         local percent2 = formatPercentage(class_data[s2[2]])
          
          
         table.insert(html, '|-')
         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',  
         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, percent1,  
             s1[1], s1[1], suffix, label1, val1,  
             s2[1], s2[1], suffix, label2, percent2
             s2[1], s2[1], suffix, label2, val2
         ))
         ))
     end
     end
Line 145: Line 184:
end
end


-- Pour l'instant, un message de secours pour le bloc sous-classe si tu n'en as pas besoin immédiatement
function p.getSubclassesBlock(frame)
function p.getSubclassesBlock(frame)
     return ""
     return ""

Revision as of 08:53, 30 May 2026

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 le nom d'usage de la classe à son identifiant Cargo (class_id)
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", -- Note : si le standard est "might_dungeon", le script s'adaptera aussi
    ["knight"]       = "might_human", 
    ["warden"]       = "might_nature",
    ["death knight"] = "might_undead",
    ["oathkeeper"]   = "might_unfrozen"
}

-- Ta structure de compétences (Matrice 12x2 complète)
local skills_structure = {
    { {"Offence", "", "skill_assault"}, {"Defence", "", "skill_protection"} },
    { {"Resistance", "", "skill_resistance"}, {"Battlecraft", "", "skill_formation"} },
    { {"Sorcery", "", "skill_sorcery"}, {"Intelligence", "", "skill_mastery"} },
    { {"Summon Avatar", "", "skill_summoner"}, {"Battle Magic", "", "skill_battlemage"} },
    { {"Daylight Magic", "", "skill_magic_day"}, {"Nightshade Magic", "", "skill_magic_night"} },
    { {"Arcane Magic", "", "skill_magic_space"}, {"Primal Magic", "", "skill_magic_primal"} },
    { {"Leadership", "", "skill_leadership"}, {"Luck", "", "skill_luck"} },
    { {"Insight", "", "skill_enlightenment"}, {"Diplomacy", "", "skill_diplomacy"} },
    { {"Logistics", "", "skill_logistic"}, {"Scouting", "", "skill_scouting"} },
    { {"Economy", "", "skill_economy"}, {"Tactics", "", "skill_tactics"} },
    { {"Siegecraft", "", "skill_siege"}, {"Recruitment", "", "skill_trainer"} },
    { {"Thaumaturgy", "", "skill_wisdom"}, {"Combat", "", "skill_battle_artistry"} }
}

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

-- Détecte automatiquement la langue via le titre (ex: "Chevalier/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", "" 
    end
end

-- Génère automatiquement le nom de la table SQL à partir du class_id
-- Exemple : "might_human" -> "humans_might_skills_table"
-- Exemple : "magic_demon" -> "demons_magic_skills_table"
local function generateTableId(classId)
    local type_part, faction_part = string.match(classId, "([a-z]+)_([a-z]+)")
    
    -- Inversion pour gérer le cas particulier de "dungeon_might" si nécessaire
    if not type_part or not faction_part then
        faction_part, type_part = string.match(classId, "([a-z]+)_([a-z]+)")
    end
    
    if faction_part and type_part then
        -- Gestion du pluriel automatique pour les noms de factions Anglaises courantes dans tes tables
        if faction_part == "human" then faction_part = "humans"
        elseif faction_part == "demon" then faction_part = "demons"
        elseif faction_part == "dungeon" then faction_part = "dungeons"
        elseif faction_part == "nature" then faction_part = "natures"
        elseif faction_part == "undead" then faction_part = "undeads"
        elseif faction_part == "unfrozen" then faction_part = "unfrozens"
        end
        
        return string.format("%s_%s_skills_table", faction_part, type_part)
    end
    return nil
end

-- 1. RÉCUPÉRATION DES POIDS (Depuis ta table Cargo SkillRollWeight)
local function fetchWeightsFromCargo(tableId)
    local weights = {}
    if not tableId then return weights end
    
    local res = mw.ext.cargo.query("SkillRollWeight", "skill_id, weight", {
        where = string.format("table_id = '%s' AND band_kind = 'default'", tableId),
        limit = 100
    })
    
    if res then
        for _, row in ipairs(res) do
            if row.skill_id and row.weight then
                weights[string.lower(mw.text.trim(row.skill_id))] = row.weight
            end
        end
    end
    return weights
end

-- 2. RÉCUPÉRATION DES TRADUCTIONS (Depuis ta table Cargo Translation)
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
    
    -- Secours anglais si la traduction locale a des 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

-- =========================================================================
-- FONCTION PRINCIPALE
-- =========================================================================

function p.getSkillsTable(frame)
    local args = frame.args[1] and frame.args or frame:getParent().args
    local lang, suffix = getLanguageInfo()
    
    local raw_class_name = string.lower(mw.text.trim(args.class or args[1] or ""))
    local class_id = class_mapping[raw_class_name]
    
    if not class_id then
        return ""
    end

    -- Génération dynamique de l'ID de la table de tirage (ex: "humans_might_skills_table")
    local table_id = generateTableId(class_id)

    -- Appel Cargo 1 : Récupération des poids
    local weight_dictionary = fetchWeightsFromCargo(table_id)
    
    -- Appel Cargo 2 : Récupération des traductions
    local translation_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 = translation_dictionary[s1[3]] or s1[1]
        local label2 = translation_dictionary[s2[3]] or s2[1]
        
        local val1 = weight_dictionary[s1[3]] or "0"
        local val2 = weight_dictionary[s2[3]] or "0"
        
        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, val1, 
            s2[1], s2[1], suffix, label2, val2
        ))
    end
    
    table.insert(html, '|}')
    return table.concat(html, "\n")
end

function p.getSubclassesBlock(frame)
    return ""
end

return p