Module:HeroClass: Difference between revisions
From Heroes of Might and Magic: Olden Era Official Wiki
No edit summary Tag: Manual revert |
No edit summary |
||
| (5 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"] | ["overlord"] = "might_dungeon", | ||
["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 171: | 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"] | ||
| Line 262: | Line 263: | ||
de = "Chance, sekundäre Fähigkeiten zu erlernen", | de = "Chance, sekundäre Fähigkeiten zu erlernen", | ||
es = "Probabilidad de aprender habilidades secundarias", | 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"] | local section_text = section_titles[lang] or section_titles["en"] | ||
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 theTranslationtable.
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, andTranslation. - The
class_mappingtable 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 (
#1a1f26background).
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


