Module:HeroClass: Difference between revisions

From Heroes of Might and Magic: Olden Era Official Wiki
No edit summary
No edit summary
 
(23 intermediate revisions by the same user not shown)
Line 14: Line 14:
     ["riftspeaker"]  = "magic_unfrozen",
     ["riftspeaker"]  = "magic_unfrozen",
     ["enforcer"]    = "might_demon",
     ["enforcer"]    = "might_demon",
     ["overlord"]     = "dungeon_might",
     ["overlord"] = "might_dungeon",  
    ["knight"]      = "might_human",  
     ["warden"]      = "might_nature",
     ["warden"]      = "might_nature",
     ["death knight"] = "might_undead",
     ["death knight"] = "might_undead",
     ["oathkeeper"]  = "might_unfrozen"
     ["oathkeeper"]  = "might_unfrozen",
    ["knight"]      = "might_human" -- AJOUTE CETTE LIGNE
}
}


Line 69: Line 69:
end
end


-- Génère automatiquement le nom de la table SQL à partir du class_id
-- Remplace ta fonction actuelle par celle-ci
local function generateTableId(classId)
local function generateTableId(classId)
     local type_part, faction_part = parseClassId(classId)
     local type_part, faction_part = parseClassId(classId)
      
      
     if faction_part and type_part then
     -- Table de correspondance explicite pour les exceptions
         if faction_part == "human" then faction_part = "humans"
    local table_names = {
         elseif faction_part == "demon" then faction_part = "demons"
         ["human"]    = "humans",
         elseif faction_part == "dungeon" then faction_part = "dungeons"
         ["demon"]    = "demons",
         elseif faction_part == "nature" then faction_part = "natures"
         ["undead"]  = "necros",  -- Ici on gère ton exception "undead -> necros"
         elseif faction_part == "undead" then faction_part = "undeads"
         ["nature"]  = "nature",  -- Pas de 's'
         elseif faction_part == "unfrozen" then faction_part = "unfrozens"
         ["dungeon"]  = "dungeon",  -- Pas de 's'
         end
         ["unfrozen"] = "unfrozen" -- Pas de 's'
         return string.format("%s_%s_skills_table", faction_part, type_part)
    }
   
    local faction_key = table_names[faction_part] or faction_part
   
    if faction_key and type_part then
         -- On assemble selon le format standard : "faction_type_skills_table"
         return string.format("%s_%s_skills_table", faction_key, type_part)
     end
     end
     return nil
     return nil
Line 147: Line 153:
-- =========================================================================
-- =========================================================================


-- 1. FONCTION INTEGRÉE : Génère le tableau des statistiques de base (100% Cargo dynamique)
-- 1. Génère le tableau des statistiques de base avec les colonnes 3 et 4 en %
-- Syntaxe : {{#invoke:HeroClass|getStatsTable|class=NOM_CLASSE}}
function p.getStatsTable(frame)
function p.getStatsTable(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
Line 160: Line 165:
     end
     end


    -- Étape A : Traduction automatique des en-têtes de colonnes structurelles
     local headers = {
     local headers = {
         fr = { char = "Carac.", base = "Base", lvl1 = "Niv. 1-12", lvl24 = "Niv. 13+" },
         fr = { char = "Carac.", base = "Base", lvl1 = "Niv. 1-12", lvl24 = "Niv. 13+" },
Line 167: Line 171:
         es = { char = "Atrib.", base = "Base", lvl1 = "Niv. 1-12", lvl24 = "Niv. 13+" },
         es = { char = "Atrib.", base = "Base", lvl1 = "Niv. 1-12", lvl24 = "Niv. 13+" },
         it = { char = "Stat", base = "Base", lvl1 = "Liv. 1-12", lvl24 = "Liv. 13+" },
         it = { char = "Stat", base = "Base", lvl1 = "Liv. 1-12", lvl24 = "Liv. 13+" },
         pl = { char = "Atryb.", base = "Bazowy", lvl1 = "Poz. 1-12", lvl24 = "Poz. 13+" }
         pl = { char = "Atryb.", base = "Bazowy", lvl1 = "Poz. 1-12", lvl24 = "Poz. 13+" },
        ru = { char = "Параметр", base = "База", lvl1 = "1-12", lvl24 = "13+" },
     }
     }
     local head = headers[lang] or headers["en"]
     local head = headers[lang] or headers["en"]


    -- Étape B : Requête Cargo pour récupérer les statistiques brutes du héros
     local fields_safe = "offence, roll_lvl1_attack, roll_lvl24_attack, defence, roll_lvl1_defense, roll_lvl24_defense, spell_power, roll_lvl1_power, roll_lvl24_power, intelligence, roll_lvl1_knowledge, roll_lvl24_knowledge"
     local fields_safe = "offence, roll_lvl1_attack, roll_lvl24_attack, defence, roll_lvl1_defense, roll_lvl24_defense, spell_power, roll_lvl1_power, roll_lvl24_power, intelligence, roll_lvl1_knowledge, roll_lvl24_knowledge"
     local res = mw.ext.cargo.query('HeroClass', fields_safe, { where = string.format('id="%s"', class_id), limit = 1 })
     local res = mw.ext.cargo.query('HeroClass', fields_safe, { where = string.format('id="%s"', class_id), limit = 1 })
Line 180: Line 184:
     local data = res[1]
     local data = res[1]


-- Étape C : Récupération des noms de stats TRADUITS depuis Cargo (type = hero_stat)
     local stat_ids = { "offence", "defence", "spell_power", "intelligence" }
     local stat_ids = { "offence", "defence", "spell_power", "intelligence" }
     local stat_names = {}
     local stat_names = {}
Line 186: Line 189:
     for _, id in ipairs(stat_ids) do
     for _, id in ipairs(stat_ids) do
         local name_found = nil
         local name_found = nil
       
        -- Correction du target_id pour coller EXACTEMENT aux pages Cargo (spell_power -> spellPower)
         local cargo_target_id = id
         local cargo_target_id = id
         if id == "spell_power" then cargo_target_id = "spellPower" end
         if id == "spell_power" then cargo_target_id = "spellPower" end
          
          
        -- Recherche dans la langue locale
         local trans_res = mw.ext.cargo.query("Translation", "name", {
         local trans_res = mw.ext.cargo.query("Translation", "name", {
             where = string.format("target_id = '%s' AND type = 'hero_stat' AND language = '%s'", cargo_target_id, lang),
             where = string.format("target_id = '%s' AND type = 'hero_stat' AND language = '%s'", cargo_target_id, lang),
Line 197: Line 197:
         })
         })
          
          
        -- Secours Anglais si pas de traduction locale
         if (not trans_res or #trans_res == 0) and lang ~= "en" then
         if (not trans_res or #trans_res == 0) and lang ~= "en" then
             trans_res = mw.ext.cargo.query("Translation", "name", {
             trans_res = mw.ext.cargo.query("Translation", "name", {
Line 209: Line 208:
         end
         end
          
          
        -- Sécurité ultime si Cargo est vide (fallbacks par défaut capitalisés)
         if not name_found or name_found == "" then
         if not name_found or name_found == "" then
             if id == "offence" then name_found = "Attack"
             if id == "offence" then name_found = "Attack"
Line 221: Line 219:
     end
     end


    -- Étape D : Construction du tableau HTML avec toutes les données Cargo unifiées
     local html = {}
     local html = {}
     table.insert(html, '{| class="wikitable" style="width:100%; border-collapse:collapse; margin-bottom:12px;"')
     table.insert(html, '{| class="wikitable" style="width:100%; border-collapse:collapse; margin-bottom:0px;"')
     table.insert(html, string.format('! style="width:34%%;" | %s || style="width:33%%; text-align:center;" | %s || style="width:33%%; text-align:center;" | %s || style="width:33%%; text-align:center;" | %s', head.char, head.base, head.lvl1, head.lvl24))   -- Ligne Attaque (Offence)
   
     table.insert(html, string.format('! style="width:100%%;" | %s !! style="width:1px; white-space:nowrap; text-align:center;" | %s !! style="width:1px; white-space:nowrap; text-align:center;" | %s !! style="width:1px; white-space:nowrap; text-align:center;" | %s', head.char, head.base, head.lvl1, head.lvl24))
   
    -- Ajout du "%" concaténé aux valeurs pour les colonnes 3 et 4
     table.insert(html, '|-')
     table.insert(html, '|-')
     table.insert(html, string.format('| [[File:Icon Stats Attack.png|24px]] %s || style="text-align:center;" | %s || style="text-align:center;" | %s || style="text-align:center;" | %s', stat_names["offence"], data.offence or 0, data.roll_lvl1_attack or 0, data.roll_lvl24_attack or 0))
     table.insert(html, string.format('| [[File:Icon Stats Attack.png|24px]] %s || style="text-align:center;" | %s || style="text-align:center;" | %s%% || style="text-align:center;" | %s%%', stat_names["offence"], data.offence or 0, data.roll_lvl1_attack or 0, data.roll_lvl24_attack or 0))
      
      
    -- Ligne Défense (Defence)
     table.insert(html, '|-')
     table.insert(html, '|-')
     table.insert(html, string.format('| [[File:Icon Stats Defence.png|24px]] %s || style="text-align:center;" | %s || style="text-align:center;" | %s || style="text-align:center;" | %s', stat_names["defence"], data.defence or 0, data.roll_lvl1_defense or 0, data.roll_lvl24_defense or 0))
     table.insert(html, string.format('| [[File:Icon Stats Defence.png|24px]] %s || style="text-align:center;" | %s || style="text-align:center;" | %s%% || style="text-align:center;" | %s%%', stat_names["defence"], data.defence or 0, data.roll_lvl1_defense or 0, data.roll_lvl24_defense or 0))
      
      
    -- Ligne Puissance (Spell_power)
     table.insert(html, '|-')
     table.insert(html, '|-')
     table.insert(html, string.format('| [[File:Icon Stats Spellpower.png|24px]] %s || style="text-align:center;" | %s || style="text-align:center;" | %s || style="text-align:center;" | %s', stat_names["spell_power"], data.spell_power or 0, data.roll_lvl1_power or 0, data.roll_lvl24_power or 0))
     table.insert(html, string.format('| [[File:Icon Stats Spellpower.png|24px]] %s || style="text-align:center;" | %s || style="text-align:center;" | %s%% || style="text-align:center;" | %s%%', stat_names["spell_power"], data.spell_power or 0, data.roll_lvl1_power or 0, data.roll_lvl24_power or 0))
      
      
    -- Ligne Savoir (Intelligence)
     table.insert(html, '|-')
     table.insert(html, '|-')
     table.insert(html, string.format('| [[File:Icon Stats Inteligence.png|24px]] %s || style="text-align:center;" | %s || style="text-align:center;" | %s || style="text-align:center;" | %s', stat_names["intelligence"], data.intelligence or 0, data.roll_lvl1_knowledge or 0, data.roll_lvl24_knowledge or 0))
     table.insert(html, string.format('| [[File:Icon Stats Inteligence.png|24px]] %s || style="text-align:center;" | %s || style="text-align:center;" | %s%% || style="text-align:center;" | %s%%', stat_names["intelligence"], data.intelligence or 0, data.roll_lvl1_knowledge or 0, data.roll_lvl24_knowledge or 0))
      
      
     table.insert(html, '|}')
     table.insert(html, '|}')
Line 244: Line 241:
end
end


-- 2. Génère le grand tableau des compétences secondaires
-- 2. Génère le grand tableau des compétences secondaires (Nettoyé du surlignage)
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
Line 259: Line 256:
     local weight_dictionary = fetchWeightsFromCargo(table_id)
     local weight_dictionary = fetchWeightsFromCargo(table_id)
     local translation_dictionary = fetchTranslationsFromCargo(lang)
     local translation_dictionary = fetchTranslationsFromCargo(lang)
   
    -- Dictionnaire pour le titre de transition entre les deux tableaux
    local section_titles = {
        fr = "Chances d'apprendre des compétences secondaires",
        en = "Chance to learn Secondary Skills",
        de = "Chance, sekundäre Fähigkeiten zu erlernen",
        es = "Probabilidad de aprender habilidades secundarias",
        pl = "Szansa na naukę umiejętności drugorzędnych",
        ru = "Вероятность выучить вторичный навык"
    }
    local section_text = section_titles[lang] or section_titles["en"]
    -- Dictionnaire local de secours pour l'en-tête de colonne du tableau
    local skill_headers = {
        fr = "Compétence",
        en = "Skill",
        de = "Fähigkeit",
        es = "Habilidad",
        it = "Abilità",
        pl = "Umiejętność"
    }
    local skill_text = skill_headers[lang] or skill_headers["en"]
      
      
     local html = {}
     local html = {}
     table.insert(html, '{| class="wikitable" style="width:100%; border-collapse:collapse;"')
   
    -- Titre de transition épuré (gras, centré, sans effet de surlignage rectangulaire)
    table.insert(html, string.format('<div style="text-align: center; font-weight:bold; font-size:1.15em; margin-top:16px; margin-bottom:12px; color:#fff;">%s</div>', section_text))
   
    -- Ouverture du tableau
     table.insert(html, '{| class="wikitable" style="width:100%; border-collapse:collapse; margin-bottom:0px;"')
   
    -- Ligne d'en-tête optimisée : les colonnes % font 1px de large minimum pour coller au texte sans retour à la ligne
    table.insert(html, string.format('! style="width:50%%;" | %s !! style="width:1px; white-space:nowrap; text-align:center;" | %% !! style="width:50%%;" | %s !! style="width:1px; white-space:nowrap; text-align:center;" | %%', skill_text, skill_text))
      
      
     for _, double_row in ipairs(skills_structure) do
     for _, double_row in ipairs(skills_structure) do
Line 269: Line 296:
         local label2 = translation_dictionary[s2[3]] or s2[1]
         local label2 = translation_dictionary[s2[3]] or s2[1]
          
          
         local val1 = weight_dictionary[s1[3]] or "0"
         local raw_val1 = tonumber(weight_dictionary[s1[3]]) or 0
         local val2 = weight_dictionary[s2[3]] or "0"
         local raw_val2 = tonumber(weight_dictionary[s2[3]]) or 0
       
        local val1 = "0"
        local val2 = "0"
       
        if raw_val1 > 0 then val1 = string.format("%g", raw_val1 / 10) end
        if raw_val2 > 0 then val2 = string.format("%g", raw_val2 / 10) end
          
          
         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 || style="text-align:center;" | %s || [[File:Skill_%s.png|32px|link=%s%s]] %s || style="text-align:center;" | %s',  
             s1[1], s1[1], suffix, label1, val1,  
             s1[1], s1[1], suffix, label1, val1,  
             s2[1], s2[1], suffix, label2, val2
             s2[1], s2[1], suffix, label2, val2
Line 311: Line 344:


     local html = {}
     local html = {}
     table.insert(html, string.format('<div style="font-weight:bold; text-align:center; margin-top:16px; margin-bottom:12px; font-size:1.3em; color:white;">%s</div>', title_text))
     table.insert(html, string.format('<div style="font-weight:bold; text-align:center; margin-top:20px; margin-bottom:12px; font-size:1.3em; color:white;">%s</div>', title_text))


     for _, sub in ipairs(sub_classes) do
     for _, sub in ipairs(sub_classes) do
Line 361: Line 394:
     end
     end


    return table.concat(html, "\n")
end
-- =========================================================================
-- 4. BOÎTE GLOBALE (Englobe l'icône, les stats et les compétences)
-- =========================================================================
function p.getMainBox(frame)
    local args = frame.args[1] and frame.args or frame:getParent().args
   
    -- Récupération du nom de la classe
    local raw_class_name = mw.text.trim(args.class or args[1] or "")
    if raw_class_name == "" then return "" end
   
    -- Formatage de la première lettre en majuscule pour l'image du blason (ex: Knight.png)
    local image_class_name = string.upper(string.sub(raw_class_name, 1, 1)) .. string.sub(raw_class_name, 2)
    local html = {}
   
    -- Ouverture du grand conteneur à bordure (Largeur max 400px idéale pour une infobox latérale, ajustable)
    table.insert(html, '<div style="max-width:420px; border: 1px solid #3a3a3a; border-radius: 8px; background-color: #1a1f26; padding: 16px; box-shadow: 0 4px 10px rgba(0,0,0,0.5); font-family: sans-serif; margin: 10px 0;">')
   
    -- 1. Affichage du blason de classe tout en haut (centré)
    table.insert(html, string.format('<div style="text-align: center; margin-bottom: 16px;">[[File:%s.png|120px]]</div>', image_class_name))
   
    -- 2. Injection du tableau des statistiques de base
    table.insert(html, p.getStatsTable(frame))
   
    -- 3. Injection de la transition et du grand tableau des compétences secondaires
    table.insert(html, p.getSkillsTable(frame))
   
    -- Fermeture du conteneur
    table.insert(html, '</div>')
   
     return table.concat(html, "\n")
     return table.concat(html, "\n")
end
end


return p
return p

Latest revision as of 20:55, 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 UTILSÉES PAR TOUTES LES FONCTIONS
-- =========================================================================

-- 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"] = "might_dungeon", 
    ["warden"]       = "might_nature",
    ["death knight"] = "might_undead",
    ["oathkeeper"]   = "might_unfrozen",
    ["knight"]       = "might_human" -- AJOUTE CETTE LIGNE
}

-- Ta structure de compétences (Matrice 12x2 complète pour le tableau des skills)
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"} }
}

-- Crée un dictionnaire inversé pour retrouver le nom d'image d'un skill à partir de son ID Cargo
local skill_ids_to_images = {}
for _, double_row in ipairs(skills_structure) do
    skill_ids_to_images[double_row[1][3]] = double_row[1][1]
    skill_ids_to_images[double_row[2][3]] = double_row[2][1]
end

-- =========================================================================
-- 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

-- Sépare le type et la faction (ex: "might_human" -> type="might", faction="human")
local function parseClassId(classId)
    local type_part, faction_part = string.match(classId, "([a-z]+)_([a-z]+)")
    if not type_part or not faction_part then
        faction_part, type_part = string.match(classId, "([a-z]+)_([a-z]+)")
    end
    return type_part, faction_part
end

-- Remplace ta fonction actuelle par celle-ci
local function generateTableId(classId)
    local type_part, faction_part = parseClassId(classId)
    
    -- Table de correspondance explicite pour les exceptions
    local table_names = {
        ["human"]    = "humans",
        ["demon"]    = "demons",
        ["undead"]   = "necros",   -- Ici on gère ton exception "undead -> necros"
        ["nature"]   = "nature",   -- Pas de 's'
        ["dungeon"]  = "dungeon",  -- Pas de 's'
        ["unfrozen"] = "unfrozen"  -- Pas de 's'
    }
    
    local faction_key = table_names[faction_part] or faction_part
    
    if faction_key and type_part then
        -- On assemble selon le format standard : "faction_type_skills_table"
        return string.format("%s_%s_skills_table", faction_key, type_part)
    end
    return nil
end

-- Récupère les poids de tirage (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

-- Récupère les noms traduits des skills
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
    
    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

-- =========================================================================
-- FONCTIONS PUBLIQUES (Appelables via le Wiki)
-- =========================================================================

-- 1. Génère le tableau des statistiques de base avec les colonnes 3 et 4 en %
function p.getStatsTable(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

    local headers = {
        fr = { char = "Carac.", base = "Base", lvl1 = "Niv. 1-12", lvl24 = "Niv. 13+" },
        en = { char = "Stat", base = "Base", lvl1 = "Lvl 1-12", lvl24 = "Lvl 13+" },
        de = { char = "Stat", base = "Basis", lvl1 = "Stufe 1-12", lvl24 = "Stufe 13+" },
        es = { char = "Atrib.", base = "Base", lvl1 = "Niv. 1-12", lvl24 = "Niv. 13+" },
        it = { char = "Stat", base = "Base", lvl1 = "Liv. 1-12", lvl24 = "Liv. 13+" },
        pl = { char = "Atryb.", base = "Bazowy", lvl1 = "Poz. 1-12", lvl24 = "Poz. 13+" },
        ru = { char = "Параметр", base = "База", lvl1 = "1-12", lvl24 = "13+" },
    }
    local head = headers[lang] or headers["en"]

    local fields_safe = "offence, roll_lvl1_attack, roll_lvl24_attack, defence, roll_lvl1_defense, roll_lvl24_defense, spell_power, roll_lvl1_power, roll_lvl24_power, intelligence, roll_lvl1_knowledge, roll_lvl24_knowledge"
    local res = mw.ext.cargo.query('HeroClass', fields_safe, { where = string.format('id="%s"', class_id), limit = 1 })

    if not res or not res[1] then 
        return "" 
    end
    local data = res[1]

    local stat_ids = { "offence", "defence", "spell_power", "intelligence" }
    local stat_names = {}

    for _, id in ipairs(stat_ids) do
        local name_found = nil
        local cargo_target_id = id
        if id == "spell_power" then cargo_target_id = "spellPower" end
        
        local trans_res = mw.ext.cargo.query("Translation", "name", {
            where = string.format("target_id = '%s' AND type = 'hero_stat' AND language = '%s'", cargo_target_id, lang),
            limit = 1
        })
        
        if (not trans_res or #trans_res == 0) and lang ~= "en" then
            trans_res = mw.ext.cargo.query("Translation", "name", {
                where = string.format("target_id = '%s' AND type = 'hero_stat' AND language = 'en'", cargo_target_id),
                limit = 1
            })
        end
        
        if trans_res and trans_res[1] then
            name_found = trans_res[1].name
        end
        
        if not name_found or name_found == "" then
            if id == "offence" then name_found = "Attack"
            elseif id == "defence" then name_found = "Defense"
            elseif id == "spell_power" then name_found = "Spellpower"
            elseif id == "intelligence" then name_found = "Knowledge"
            end
        end
        
        stat_names[id] = name_found
    end

    local html = {}
    table.insert(html, '{| class="wikitable" style="width:100%; border-collapse:collapse; margin-bottom:0px;"')
    
    table.insert(html, string.format('! style="width:100%%;" | %s !! style="width:1px; white-space:nowrap; text-align:center;" | %s !! style="width:1px; white-space:nowrap; text-align:center;" | %s !! style="width:1px; white-space:nowrap; text-align:center;" | %s', head.char, head.base, head.lvl1, head.lvl24))
    
    -- Ajout du "%" concaténé aux valeurs pour les colonnes 3 et 4
    table.insert(html, '|-')
    table.insert(html, string.format('| [[File:Icon Stats Attack.png|24px]] %s || style="text-align:center;" | %s || style="text-align:center;" | %s%% || style="text-align:center;" | %s%%', stat_names["offence"], data.offence or 0, data.roll_lvl1_attack or 0, data.roll_lvl24_attack or 0))
    
    table.insert(html, '|-')
    table.insert(html, string.format('| [[File:Icon Stats Defence.png|24px]] %s || style="text-align:center;" | %s || style="text-align:center;" | %s%% || style="text-align:center;" | %s%%', stat_names["defence"], data.defence or 0, data.roll_lvl1_defense or 0, data.roll_lvl24_defense or 0))
    
    table.insert(html, '|-')
    table.insert(html, string.format('| [[File:Icon Stats Spellpower.png|24px]] %s || style="text-align:center;" | %s || style="text-align:center;" | %s%% || style="text-align:center;" | %s%%', stat_names["spell_power"], data.spell_power or 0, data.roll_lvl1_power or 0, data.roll_lvl24_power or 0))
    
    table.insert(html, '|-')
    table.insert(html, string.format('| [[File:Icon Stats Inteligence.png|24px]] %s || style="text-align:center;" | %s || style="text-align:center;" | %s%% || style="text-align:center;" | %s%%', stat_names["intelligence"], data.intelligence or 0, data.roll_lvl1_knowledge or 0, data.roll_lvl24_knowledge or 0))
    
    table.insert(html, '|}')
    return table.concat(html, "\n")
end

-- 2. Génère le grand tableau des compétences secondaires (Nettoyé du surlignage)
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

    local table_id = generateTableId(class_id)
    local weight_dictionary = fetchWeightsFromCargo(table_id)
    local translation_dictionary = fetchTranslationsFromCargo(lang)
    
    -- Dictionnaire pour le titre de transition entre les deux tableaux
    local section_titles = {
        fr = "Chances d'apprendre des compétences secondaires",
        en = "Chance to learn Secondary Skills",
        de = "Chance, sekundäre Fähigkeiten zu erlernen",
        es = "Probabilidad de aprender habilidades secundarias",
        pl = "Szansa na naukę umiejętności drugorzędnych",
        ru = "Вероятность выучить вторичный навык"
    }
    local section_text = section_titles[lang] or section_titles["en"]

    -- Dictionnaire local de secours pour l'en-tête de colonne du tableau
    local skill_headers = {
        fr = "Compétence",
        en = "Skill",
        de = "Fähigkeit",
        es = "Habilidad",
        it = "Abilità",
        pl = "Umiejętność"
    }
    local skill_text = skill_headers[lang] or skill_headers["en"]
    
    local html = {}
    
    -- Titre de transition épuré (gras, centré, sans effet de surlignage rectangulaire)
    table.insert(html, string.format('<div style="text-align: center; font-weight:bold; font-size:1.15em; margin-top:16px; margin-bottom:12px; color:#fff;">%s</div>', section_text))
    
    -- Ouverture du tableau
    table.insert(html, '{| class="wikitable" style="width:100%; border-collapse:collapse; margin-bottom:0px;"')
    
    -- Ligne d'en-tête optimisée : les colonnes % font 1px de large minimum pour coller au texte sans retour à la ligne
    table.insert(html, string.format('! style="width:50%%;" | %s !! style="width:1px; white-space:nowrap; text-align:center;" | %% !! style="width:50%%;" | %s !! style="width:1px; white-space:nowrap; text-align:center;" | %%', skill_text, skill_text))
    
    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 raw_val1 = tonumber(weight_dictionary[s1[3]]) or 0
        local raw_val2 = tonumber(weight_dictionary[s2[3]]) or 0
        
        local val1 = "0"
        local val2 = "0"
        
        if raw_val1 > 0 then val1 = string.format("%g", raw_val1 / 10) end
        if raw_val2 > 0 then val2 = string.format("%g", raw_val2 / 10) end
        
        table.insert(html, '|-')
        table.insert(html, string.format('| [[File:Skill_%s.png|32px|link=%s%s]] %s || style="text-align:center;" | %s || [[File:Skill_%s.png|32px|link=%s%s]] %s || style="text-align:center;" | %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

-- 3. Génère le bloc des sous-classes
function p.getSubclassesBlock(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
    
    local type_part, faction_part = parseClassId(class_id)
    if not type_part or not faction_part 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 fields = "id, icon, activation_skill_1_sid, activation_skill_2_sid, activation_skill_3_sid, activation_skill_4_sid, activation_skill_5_sid"

    local sub_classes = mw.ext.cargo.query("HeroSubClass", fields, {
        where = string.format("faction = '%s' AND class_type = '%s'", faction_part, type_part),
        limit = 10
    })
    
    if not sub_classes or #sub_classes == 0 then 
        return "" 
    end

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

    for _, sub in ipairs(sub_classes) do
        local sub_id = sub.id
        local name_trans = sub_id
        local name_en = sub_id
        local desc_trans = ""
        
        local trans_res = mw.ext.cargo.query("Translation", "name, description", {
            where = string.format("target_id = '%s' AND type = 'hero_sub_class' AND language = '%s'", sub_id, lang),
            limit = 1
        })
        
        if trans_res and trans_res[1] then
            name_trans = trans_res[1].name or sub_id
            desc_trans = trans_res[1].description or ""
        end

        if lang ~= "en" then
            local eng_res = mw.ext.cargo.query("Translation", "name", {
                where = string.format("target_id = '%s' AND type = 'hero_sub_class' AND language = 'en'", sub_id),
                limit = 1
            })
            if eng_res and eng_res[1] then
                name_en = eng_res[1].name or sub_id
            end
        else
            name_en = name_trans
        end

        table.insert(html, '<div style="margin:12px 0; border:1px solid rgba(255,255,255,0.1); border-radius:8px; padding:12px; background: rgba(0,0,0,0.15);">')
        table.insert(html, string.format('  <div style="display:flex; justify-content:center; align-items:center; gap:10px; font-weight:bold; font-size:1.25em; color:#ddd; margin-bottom:8px;">[[File:%s.png|60px]] <span>%s</span></div>', name_en, name_trans))
        
        if desc_trans ~= "" then
            table.insert(html, string.format('  <div style="text-align:center; margin-bottom:12px; font-style:italic; color:#aaa; font-size:1em; padding: 0 20px;">%s</div>', desc_trans))
        end
        
        local images_row = {}
        for i = 1, 5 do
            local skill_id = sub["activation_skill_" .. i .. "_sid"]
            if skill_id and skill_id ~= "" then
                local img_name = skill_ids_to_images[string.lower(mw.text.trim(skill_id))] or "Unknown"
                table.insert(images_row, string.format('<div style="display:inline-block; margin: 0 6px;">[[File:Skill_%s.png|50px|link=%s%s]]</div>', img_name, img_name, suffix))
            end
        end
        
        table.insert(html, '<div style="display:block; text-align:center; width:100%; margin-top:8px;">' .. table.concat(images_row, "") .. '</div>')
        table.insert(html, '</div>')
    end

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

-- =========================================================================
-- 4. BOÎTE GLOBALE (Englobe l'icône, les stats et les compétences)
-- =========================================================================
function p.getMainBox(frame)
    local args = frame.args[1] and frame.args or frame:getParent().args
    
    -- Récupération du nom de la classe
    local raw_class_name = mw.text.trim(args.class or args[1] or "")
    if raw_class_name == "" then return "" end
    
    -- Formatage de la première lettre en majuscule pour l'image du blason (ex: Knight.png)
    local image_class_name = string.upper(string.sub(raw_class_name, 1, 1)) .. string.sub(raw_class_name, 2)

    local html = {}
    
    -- Ouverture du grand conteneur à bordure (Largeur max 400px idéale pour une infobox latérale, ajustable)
    table.insert(html, '<div style="max-width:420px; border: 1px solid #3a3a3a; border-radius: 8px; background-color: #1a1f26; padding: 16px; box-shadow: 0 4px 10px rgba(0,0,0,0.5); font-family: sans-serif; margin: 10px 0;">')
    
    -- 1. Affichage du blason de classe tout en haut (centré)
    table.insert(html, string.format('<div style="text-align: center; margin-bottom: 16px;">[[File:%s.png|120px]]</div>', image_class_name))
    
    -- 2. Injection du tableau des statistiques de base
    table.insert(html, p.getStatsTable(frame))
    
    -- 3. Injection de la transition et du grand tableau des compétences secondaires
    table.insert(html, p.getSkillsTable(frame))
    
    -- Fermeture du conteneur
    table.insert(html, '</div>')
    
    return table.concat(html, "\n")
end

return p