Module:Sandbox/Aurelain/SkillsOverview: Difference between revisions
From Heroes of Might and Magic: Olden Era Official Wiki
No edit summary |
No edit summary |
||
| (172 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- Usage: {{#invoke:SkillsOverview|display|lang=en}} | -- Usage: {{#invoke:SkillsOverview|display|lang=en}} | ||
local p = {} | local p = {} | ||
local | |||
'', | local CATEGORY_MIGHT = 'wiki_skills_might' | ||
' | local CATEGORY_MAGIC = 'wiki_skills_magic' | ||
' | local CATEGORY_GENERAL = 'wiki_skills_general' | ||
local CATEGORY_FACTION = 'wiki_skills_faction' | |||
local CATEGORIES = { | |||
skill_assault = CATEGORY_MIGHT, -- Offense | |||
skill_battle_artistry = CATEGORY_MIGHT, -- Combat | |||
skill_battlemage = CATEGORY_MAGIC, -- Battle Magic | |||
skill_diplomacy = CATEGORY_GENERAL, -- Diplomacy | |||
skill_economy = CATEGORY_GENERAL, -- Economy | |||
skill_enlightenment = CATEGORY_GENERAL, -- Insight | |||
skill_faction_demons = CATEGORY_FACTION, -- Hive | |||
skill_faction_dungeon = CATEGORY_FACTION, -- Dungeon | |||
skill_faction_humans = CATEGORY_FACTION, -- Temple | |||
skill_faction_nature = CATEGORY_FACTION, -- Grove | |||
skill_faction_undead = CATEGORY_FACTION, -- Necropolis | |||
skill_faction_unfrozen = CATEGORY_FACTION, -- Schism | |||
skill_first_aid = CATEGORY_GENERAL, -- currently unused! | |||
skill_formation = CATEGORY_MIGHT, -- Battlecraft | |||
skill_leadership = CATEGORY_MIGHT, -- Leadership | |||
skill_logistic = CATEGORY_GENERAL, -- Logistics | |||
skill_luck = CATEGORY_MIGHT, -- Luck | |||
skill_magic_day = CATEGORY_MAGIC, -- Daylight | |||
skill_magic_night = CATEGORY_MAGIC, -- Nightshade | |||
skill_magic_primal = CATEGORY_MAGIC, -- Primal | |||
skill_magic_space = CATEGORY_MAGIC, -- Arcane | |||
skill_mastery = CATEGORY_MAGIC, -- Wisdom | |||
skill_protection = CATEGORY_MIGHT, -- Defense | |||
skill_resistance = CATEGORY_MIGHT, -- Resistance | |||
skill_scouting = CATEGORY_GENERAL, -- Scouting | |||
skill_siege = CATEGORY_MIGHT, -- Siegecraft | |||
skill_sorcery = CATEGORY_MAGIC, -- Sorcery | |||
skill_summoner = CATEGORY_MAGIC, -- Summon Avatar | |||
skill_tactics = CATEGORY_MIGHT, -- Tactics | |||
skill_trainer = CATEGORY_MIGHT, -- Recruitment | |||
skill_wisdom = CATEGORY_MAGIC, -- Thaumaturgy | |||
} | |||
local ORDER = { | |||
'skill_assault', -- Offense | |||
'skill_protection', -- Defense | |||
'skill_leadership', -- Leadership | |||
'skill_luck', -- Luck | |||
'skill_resistance', -- Resistance | |||
'skill_tactics', -- Tactics | |||
'skill_formation', -- Battlecraft | |||
'skill_siege', -- Siegecraft | |||
'skill_trainer', -- Recruitment | |||
'skill_battle_artistry', -- Combat | |||
'skill_battlemage', -- Battle Magic | |||
'skill_sorcery', -- Sorcery | |||
'skill_summoner', -- Summon Avatar | |||
'skill_mastery', -- Wisdom | |||
'skill_magic_day', -- Daylight | |||
'skill_magic_night', -- Nightshade | |||
'skill_magic_space', -- Arcane | |||
'skill_magic_primal', -- Primal | |||
'skill_wisdom', -- Thaumaturgy | |||
'skill_diplomacy', -- Diplomacy | |||
'skill_logistic', -- Logistics | |||
'skill_scouting', -- Scouting | |||
'skill_enlightenment', -- Insight | |||
'skill_economy', -- Economy | |||
'skill_faction_humans', -- Temple | |||
'skill_faction_undead', -- Necropolis | |||
'skill_faction_nature', -- Grove | |||
'skill_faction_demons', -- Hive | |||
'skill_faction_unfrozen', -- Schism | |||
'skill_faction_dungeon', -- Dungeon | |||
'skill_first_aid', -- currently unused! | |||
} | |||
-- Note: The right side is just a fallback, it will seldom be used. | |||
local TRANSLATION_IDS = { | |||
wiki_skills_might = 'Might Skills', | |||
wiki_skills_magic = 'Magic Skills', | |||
wiki_skills_general = 'General Skills', | |||
wiki_skills_faction = 'Faction Skills', | |||
} | |||
local PATCH_NAME = { | |||
['Summon Avatar'] = 'Summon Avatar (Skill)', | |||
} | } | ||
------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ||
-- Debugs a variable | -- Debugs a variable | ||
| Line 11: | Line 92: | ||
local function dump(target) | local function dump(target) | ||
return '<pre>' .. mw.dumpObject(target) .. '</pre>' | return '<pre>' .. mw.dumpObject(target) .. '</pre>' | ||
end | |||
------------------------------------------------------------------------------------------------------------------------ | |||
-- Spawns a safe robotic url from an English name, fit for adding it before the visible name | |||
------------------------------------------------------------------------------------------------------------------------ | |||
local function getUrl(nameEn, lang) | |||
local url = PATCH_NAME[nameEn] or nameEn | |||
if lang ~= 'en' then | |||
url = url .. '/' .. lang | |||
end | |||
return url | |||
end | |||
------------------------------------------------------------------------------------------------------------------------ | |||
-- Retrieves the text for some specific ids from Cargo Translations. | |||
------------------------------------------------------------------------------------------------------------------------ | |||
local function translateIds(ids, lang, extra) | |||
-- Key list | |||
local list = {} | |||
for key, _ in pairs(ids) do | |||
list[#list + 1] = key | |||
end | |||
local idListString = '"' .. table.concat(list, '", "') .. '"' | |||
-- Cargo | |||
local where = {} | |||
table.insert(where, 'target_id IN (' .. idListString .. ')') | |||
table.insert(where, 'language = "' .. lang .. '"') | |||
if extra then | |||
table.insert(where, extra) | |||
end | |||
local results = mw.ext.cargo.query('Translation', 'target_id, name', { | |||
where = table.concat(where, ' AND '), | |||
limit = 100 | |||
}) | |||
-- Dictionary | |||
local dictionary = mw.clone(ids) | |||
for key, value in pairs(dictionary) do | |||
dictionary[key] = value .. '[[Data:WikiTranslations/' .. lang .. '#' .. key .. '|💬]]' | |||
end | |||
for _, row in ipairs(results) do | |||
dictionary[row['target_id']] = row['name'] | |||
end | |||
return dictionary | |||
end | end | ||
| Line 23: | Line 149: | ||
table.insert(fields, 'Translation.name = name'); | table.insert(fields, 'Translation.name = name'); | ||
table.insert(fields, 'Translation.description = description'); | table.insert(fields, 'Translation.description = description'); | ||
table.insert(fields, 'Translation.language = language'); | |||
-- where: | -- where: | ||
| Line 30: | Line 157: | ||
end | end | ||
table.insert(where, 'Skill.variant = "production"') | table.insert(where, 'Skill.variant = "production"') | ||
table.insert(where, 'Translation.language = "' .. lang .. '"') | table.insert(where, '(Translation.language = "en" OR Translation.language = "' .. lang .. '")') | ||
-- query: | -- query: | ||
| Line 36: | Line 163: | ||
join = 'Skill.id = Translation.target_id', | join = 'Skill.id = Translation.target_id', | ||
where = table.concat(where, ' AND '), | where = table.concat(where, ' AND '), | ||
limit = | limit = 500 | ||
}) | }) | ||
end | end | ||
| Line 77: | Line 204: | ||
table.insert(fields, 'Translation.name = name'); | table.insert(fields, 'Translation.name = name'); | ||
table.insert(fields, 'Translation.description = description'); | table.insert(fields, 'Translation.description = description'); | ||
table.insert(fields, 'Translation.language = language'); | |||
-- where: | -- where: | ||
| Line 84: | Line 212: | ||
end | end | ||
table.insert(where, 'SubSkill.variant = "production"') | table.insert(where, 'SubSkill.variant = "production"') | ||
table.insert(where, 'Translation.language = "' .. lang .. '"') | table.insert(where, '(Translation.language = "en" OR Translation.language = "' .. lang .. '")') | ||
-- query: | -- query: | ||
| Line 90: | Line 218: | ||
join = 'SubSkill.id = Translation.target_id', | join = 'SubSkill.id = Translation.target_id', | ||
where = table.concat(where, ' AND '), | where = table.concat(where, ' AND '), | ||
limit = | limit = 1000 | ||
}) | }) | ||
end | end | ||
| Line 97: | Line 225: | ||
-- Folds various skill results from Cargo into a single table | -- Folds various skill results from Cargo into a single table | ||
------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ||
local function consolidateSkills(main, iconAndOffers, subs) | local function consolidateSkills(main, iconAndOffers, subs, lang) | ||
local hub = {} | local hub = {} | ||
for _, row in ipairs(main) do | for _, row in ipairs(main) do | ||
local id = row.id | local id = row.id | ||
local variant = row.variant | local variant = row.variant | ||
hub[id] = hub[id] or { ranks = {} } | hub[id] = hub[id] or { id = id, ranks = {} } | ||
local entry = hub[id] | local entry = hub[id] | ||
if variant then | if variant then | ||
entry.ranks[ | -- Level 1, 2 or 3 | ||
name = row.name or '' | local i = tonumber(variant) or 1 | ||
description = row.description or '' | entry.ranks[i] = entry.ranks[i] or {} | ||
local rank = entry.ranks[i] | |||
if row.language == lang then | |||
rank.name = row.name or '' | |||
rank.description = row.description or '' | |||
end | |||
if row.language == 'en' then | |||
rank.nameEn = row.name or '' | |||
rank.descriptionEn = row.description or '' | |||
end | |||
else | else | ||
entry.name = row.name | -- Root level (the group/family) | ||
if row.language == lang then | |||
entry.name = row.name or '' | |||
end | |||
if row.language == 'en' then | |||
entry.nameEn = row.name or '' | |||
end | |||
end | end | ||
end | end | ||
| Line 122: | Line 264: | ||
rank.subSkills = {} | rank.subSkills = {} | ||
for _, subId in ipairs(ids) do | for _, subId in ipairs(ids) do | ||
rank.subSkills | table.insert(rank.subSkills, { | ||
id = subId | |||
}) | |||
end | end | ||
end | end | ||
| Line 131: | Line 275: | ||
for _, rank in ipairs(entry.ranks) do | for _, rank in ipairs(entry.ranks) do | ||
local subSkills = rank.subSkills or {} | local subSkills = rank.subSkills or {} | ||
if | for _, subSkill in ipairs(subSkills) do | ||
if subSkill.id == id then | |||
if row.language == lang then | |||
subSkill.icon = row.icon or '' | |||
subSkill.name = row.name or '' | |||
subSkill.description = row.description or '' | |||
end | |||
if row.language == 'en' then | |||
subSkill.descriptionEn = row.description or '' | |||
end | |||
end | |||
end | end | ||
end | end | ||
end | end | ||
return hub | return hub | ||
end | |||
------------------------------------------------------------------------------------------------------------------------ | |||
-- Converts a dictionary into a linear array | |||
------------------------------------------------------------------------------------------------------------------------ | |||
local function flatten(hub) | |||
local list = {} | |||
for _, entry in pairs(hub) do | |||
list[#list + 1] = entry | |||
end | |||
return list | |||
end | |||
------------------------------------------------------------------------------------------------------------------------ | |||
-- Converts an array into a dictionary, where the array.value becomes dic.key and array.index becomes dic.value | |||
------------------------------------------------------------------------------------------------------------------------ | |||
local function flipArray(array) | |||
local dic = {} | |||
for index, value in ipairs(array) do | |||
dic[value] = index | |||
end | |||
return dic | |||
end | |||
local ID_TO_ORDER = flipArray(ORDER) | |||
------------------------------------------------------------------------------------------------------------------------ | |||
-- Decides the order between two skills. | |||
------------------------------------------------------------------------------------------------------------------------ | |||
local function compareSkills(a, b) | |||
local rankA = ID_TO_ORDER[a.id] or 99 | |||
local rankB = ID_TO_ORDER[b.id] or 99 | |||
if rankA ~= rankB then | |||
return rankA < rankB | |||
end | |||
return a.name < b.name | |||
end | |||
------------------------------------------------------------------------------------------------------------------------ | |||
-- Moves punctuation outside the quotes | |||
------------------------------------------------------------------------------------------------------------------------ | |||
local function repairPunctuation(text) | |||
local allQuotes = [=["„“”‘’«»「」『』]=] | |||
local punctuation = "%,%.%!%?" | |||
local pattern = "([" .. punctuation .. "])([" .. allQuotes .. "])" | |||
text = mw.ustring.gsub(text, pattern, "%2%1") | |||
return text | |||
end | |||
------------------------------------------------------------------------------------------------------------------------ | |||
-- Returns a "Foo/XX" string based on the first quoted word in a text, if any. | |||
------------------------------------------------------------------------------------------------------------------------ | |||
local function findUrl(descriptionInEnglish, lang) | |||
local name = descriptionInEnglish:match("“(.-)”") | |||
if name and name ~= '' then | |||
return getUrl(name, lang) | |||
end | |||
return nil | |||
end | |||
------------------------------------------------------------------------------------------------------------------------ | |||
-- Converts quoted text into wiki links | |||
------------------------------------------------------------------------------------------------------------------------ | |||
local function replaceQuotesWithLinks(text, lang, url) | |||
local allQuotes = [=["„“”«»「」『』]=] | |||
local pattern = "([" .. allQuotes .. "])([^" .. allQuotes .. "]+)([" .. allQuotes .. "])" | |||
text = mw.ustring.gsub(text, pattern, "%1[[" .. url .. "|%2]]%3") | |||
local quotePairs = { | |||
{ '‘', '’' }, -- English "smart" single quotes | |||
} | |||
if lang == 'ko' then | |||
table.insert(quotePairs, { "'", "'" }) -- we're checking for single quotes only in Korean | |||
end | |||
for _, pair in ipairs(quotePairs) do | |||
local openQuote = pair[1] | |||
local closeQuote = pair[2] | |||
local pairedPattern = openQuote .. "([^" .. closeQuote .. "]+)" .. closeQuote | |||
text = mw.ustring.gsub(text, pairedPattern, "[[%1]]") | |||
end | |||
return text | |||
end | |||
------------------------------------------------------------------------------------------------------------------------ | |||
-- Converts the first capitalized mid-sentence words into wiki links | |||
------------------------------------------------------------------------------------------------------------------------ | |||
local function replaceCapitalizedWithLinks(text, url) | |||
text = mw.ustring.gsub(text, "([^%.]) (%u[%w ’]+)", "%1 [[" .. url .. "|%2]]") | |||
return text | |||
end | |||
------------------------------------------------------------------------------------------------------------------------ | |||
-- Adds [[link]] inside a description | |||
------------------------------------------------------------------------------------------------------------------------ | |||
local function linkifyDescription(description, descriptionEn, lang) | |||
descriptionEn = repairPunctuation(descriptionEn) | |||
local url = findUrl(descriptionEn, lang) | |||
if not url then return description end | |||
description = lang == 'en' and descriptionEn or repairPunctuation(description) | |||
local adapted = replaceQuotesWithLinks(description, lang, url) | |||
if description ~= adapted then | |||
description = adapted | |||
else | |||
description = replaceCapitalizedWithLinks(description, url) | |||
end | |||
return description | |||
end | |||
------------------------------------------------------------------------------------------------------------------------ | |||
-- Adds [[links]] inside descriptions | |||
------------------------------------------------------------------------------------------------------------------------ | |||
local function linkifyDescriptions(skills, lang) | |||
for _, group in ipairs(skills) do | |||
local ranks = group.ranks or {} | |||
for _, rank in ipairs(ranks) do | |||
local subSkills = rank.subSkills or {} | |||
for _, subSkill in pairs(subSkills) do | |||
if subSkill.description then | |||
subSkill.description = linkifyDescription(subSkill.description, subSkill.descriptionEn, lang) | |||
end | |||
end | |||
end | |||
end | |||
end | end | ||
| Line 145: | Line 422: | ||
-- Displays a single skill (icon+name+description) | -- Displays a single skill (icon+name+description) | ||
------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ||
local function createSkill(target, size) | local function createSkill(target, class, size) | ||
local root = mw.html.create('div'):addClass('box') | local root = mw.html.create('div'):addClass('box'):addClass(class) | ||
root:tag('div'):addClass('icon-container'):wikitext('[[File:' .. target.icon .. '.png|' .. size .. 'px]]') | root:tag('div'):addClass('icon-container'):wikitext('[[File:' .. target.icon .. '.png|' .. size .. 'px]]') | ||
local textContainer = root:tag('div'):addClass('text-container') | local textContainer = root:tag('div'):addClass('text-container') | ||
| Line 157: | Line 434: | ||
-- Displays the main content | -- Displays the main content | ||
------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ||
local function renderSkills(skills) | local function renderSkills(skills, words, lang) | ||
local root = mw.html.create() | local root = mw.html.create() | ||
for | local currentCategory = nil | ||
root:tag('h2'):addClass(' | local isAll = #skills > 1 | ||
for _, group in ipairs(skills) do | |||
local id = group.id | |||
local category = CATEGORIES[id] | |||
if category and category ~= currentCategory then | |||
currentCategory = category | |||
if isAll then | |||
root:tag('h2'):addClass('category'):wikitext(words[category]) | |||
end | |||
end | |||
local ranks = group.ranks or {} | local ranks = group.ranks or {} | ||
if isAll then | |||
local url = getUrl(group.nameEn, lang) | |||
root:tag('h3'):addClass('group'):wikitext('[[' .. url .. '|' .. group.name .. ']]') | |||
end | |||
root:node(createSkill(rank, | for _, rank in ipairs(ranks) do | ||
root:node(createSkill(rank, 'rank', 64)) | |||
local subSkills = rank.subSkills or {} | local subSkills = rank.subSkills or {} | ||
for _, subSkill in pairs(subSkills) do | for _, subSkill in pairs(subSkills) do | ||
root:node(createSkill(subSkill, 'sub', 32)) | |||
end | end | ||
end | end | ||
end | end | ||
return tostring(root) | return tostring(root) | ||
end | |||
------------------------------------------------------------------------------------------------------------------------ | |||
-- Bundles together all cargo skill queries: | |||
-- Also called by Module:SkillSynergies | |||
-- Also called by Module:SkillArtifacts | |||
------------------------------------------------------------------------------------------------------------------------ | |||
function p.getSkillsFromCargo(lang, skill) | |||
local main = queryMain(lang, skill) | |||
--if 1 then return main end | |||
local iconAndOffers = queryIconAndOffers(skill) | |||
local subs = querySubs(lang, skill) | |||
local hub = consolidateSkills(main, iconAndOffers, subs, lang) | |||
return hub | |||
end | end | ||
| Line 183: | Line 485: | ||
------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ||
function p.display(frame) | function p.display(frame) | ||
--if 1 then return dump(frame) end | |||
-- Args | -- Args | ||
local args = frame.args | local args = frame.args | ||
local lang = args.lang or 'en' | local lang = args.lang or 'en' | ||
local skill = args.skill | local skill = args.skill | ||
--lang = 'fr' | |||
--skill = 'skill_formation' | |||
-- Language | |||
local words = translateIds(TRANSLATION_IDS, lang) | |||
-- Cargo | -- Cargo | ||
local | local skillsHub = p.getSkillsFromCargo(lang, skill) | ||
if # | local skills = flatten(skillsHub) | ||
return ' | --if 1 then return dump(#skills) end | ||
if #skills == 0 then | |||
return 'No skills found!' | |||
end | end | ||
-- Various manipulations | |||
table.sort(skills, compareSkills) | |||
linkifyDescriptions(skills, lang) | |||
skills | |||
-- | -- Content | ||
local markup = renderSkills(skills) | local markup = renderSkills(skills, words, lang) | ||
--if 1 then return dump(markup) end | |||
-- Output | -- Output | ||
local styleTag = frame:extensionTag('templatestyles', '', { src = frame:getTitle() .. '/styles.css' }) | local styleTag = frame:extensionTag('templatestyles', '', { src = frame:getTitle() .. '/styles.css' }) | ||
return styleTag .. markup | return styleTag .. markup | ||
end | end | ||
return p | return p | ||
Latest revision as of 09:03, 15 June 2026
Documentation for this module may be created at Module:Sandbox/Aurelain/SkillsOverview/doc
-- Usage: {{#invoke:SkillsOverview|display|lang=en}}
local p = {}
local CATEGORY_MIGHT = 'wiki_skills_might'
local CATEGORY_MAGIC = 'wiki_skills_magic'
local CATEGORY_GENERAL = 'wiki_skills_general'
local CATEGORY_FACTION = 'wiki_skills_faction'
local CATEGORIES = {
skill_assault = CATEGORY_MIGHT, -- Offense
skill_battle_artistry = CATEGORY_MIGHT, -- Combat
skill_battlemage = CATEGORY_MAGIC, -- Battle Magic
skill_diplomacy = CATEGORY_GENERAL, -- Diplomacy
skill_economy = CATEGORY_GENERAL, -- Economy
skill_enlightenment = CATEGORY_GENERAL, -- Insight
skill_faction_demons = CATEGORY_FACTION, -- Hive
skill_faction_dungeon = CATEGORY_FACTION, -- Dungeon
skill_faction_humans = CATEGORY_FACTION, -- Temple
skill_faction_nature = CATEGORY_FACTION, -- Grove
skill_faction_undead = CATEGORY_FACTION, -- Necropolis
skill_faction_unfrozen = CATEGORY_FACTION, -- Schism
skill_first_aid = CATEGORY_GENERAL, -- currently unused!
skill_formation = CATEGORY_MIGHT, -- Battlecraft
skill_leadership = CATEGORY_MIGHT, -- Leadership
skill_logistic = CATEGORY_GENERAL, -- Logistics
skill_luck = CATEGORY_MIGHT, -- Luck
skill_magic_day = CATEGORY_MAGIC, -- Daylight
skill_magic_night = CATEGORY_MAGIC, -- Nightshade
skill_magic_primal = CATEGORY_MAGIC, -- Primal
skill_magic_space = CATEGORY_MAGIC, -- Arcane
skill_mastery = CATEGORY_MAGIC, -- Wisdom
skill_protection = CATEGORY_MIGHT, -- Defense
skill_resistance = CATEGORY_MIGHT, -- Resistance
skill_scouting = CATEGORY_GENERAL, -- Scouting
skill_siege = CATEGORY_MIGHT, -- Siegecraft
skill_sorcery = CATEGORY_MAGIC, -- Sorcery
skill_summoner = CATEGORY_MAGIC, -- Summon Avatar
skill_tactics = CATEGORY_MIGHT, -- Tactics
skill_trainer = CATEGORY_MIGHT, -- Recruitment
skill_wisdom = CATEGORY_MAGIC, -- Thaumaturgy
}
local ORDER = {
'skill_assault', -- Offense
'skill_protection', -- Defense
'skill_leadership', -- Leadership
'skill_luck', -- Luck
'skill_resistance', -- Resistance
'skill_tactics', -- Tactics
'skill_formation', -- Battlecraft
'skill_siege', -- Siegecraft
'skill_trainer', -- Recruitment
'skill_battle_artistry', -- Combat
'skill_battlemage', -- Battle Magic
'skill_sorcery', -- Sorcery
'skill_summoner', -- Summon Avatar
'skill_mastery', -- Wisdom
'skill_magic_day', -- Daylight
'skill_magic_night', -- Nightshade
'skill_magic_space', -- Arcane
'skill_magic_primal', -- Primal
'skill_wisdom', -- Thaumaturgy
'skill_diplomacy', -- Diplomacy
'skill_logistic', -- Logistics
'skill_scouting', -- Scouting
'skill_enlightenment', -- Insight
'skill_economy', -- Economy
'skill_faction_humans', -- Temple
'skill_faction_undead', -- Necropolis
'skill_faction_nature', -- Grove
'skill_faction_demons', -- Hive
'skill_faction_unfrozen', -- Schism
'skill_faction_dungeon', -- Dungeon
'skill_first_aid', -- currently unused!
}
-- Note: The right side is just a fallback, it will seldom be used.
local TRANSLATION_IDS = {
wiki_skills_might = 'Might Skills',
wiki_skills_magic = 'Magic Skills',
wiki_skills_general = 'General Skills',
wiki_skills_faction = 'Faction Skills',
}
local PATCH_NAME = {
['Summon Avatar'] = 'Summon Avatar (Skill)',
}
------------------------------------------------------------------------------------------------------------------------
-- Debugs a variable
------------------------------------------------------------------------------------------------------------------------
local function dump(target)
return '<pre>' .. mw.dumpObject(target) .. '</pre>'
end
------------------------------------------------------------------------------------------------------------------------
-- Spawns a safe robotic url from an English name, fit for adding it before the visible name
------------------------------------------------------------------------------------------------------------------------
local function getUrl(nameEn, lang)
local url = PATCH_NAME[nameEn] or nameEn
if lang ~= 'en' then
url = url .. '/' .. lang
end
return url
end
------------------------------------------------------------------------------------------------------------------------
-- Retrieves the text for some specific ids from Cargo Translations.
------------------------------------------------------------------------------------------------------------------------
local function translateIds(ids, lang, extra)
-- Key list
local list = {}
for key, _ in pairs(ids) do
list[#list + 1] = key
end
local idListString = '"' .. table.concat(list, '", "') .. '"'
-- Cargo
local where = {}
table.insert(where, 'target_id IN (' .. idListString .. ')')
table.insert(where, 'language = "' .. lang .. '"')
if extra then
table.insert(where, extra)
end
local results = mw.ext.cargo.query('Translation', 'target_id, name', {
where = table.concat(where, ' AND '),
limit = 100
})
-- Dictionary
local dictionary = mw.clone(ids)
for key, value in pairs(dictionary) do
dictionary[key] = value .. '[[Data:WikiTranslations/' .. lang .. '#' .. key .. '|💬]]'
end
for _, row in ipairs(results) do
dictionary[row['target_id']] = row['name']
end
return dictionary
end
------------------------------------------------------------------------------------------------------------------------
-- Retrieves the main skill translations from Cargo
------------------------------------------------------------------------------------------------------------------------
local function queryMain(lang, forcedSkill)
-- fields:
local fields = {}
table.insert(fields, 'Skill.id = id');
table.insert(fields, 'Translation.variant = variant');
table.insert(fields, 'Translation.name = name');
table.insert(fields, 'Translation.description = description');
table.insert(fields, 'Translation.language = language');
-- where:
local where = {}
if forcedSkill then
table.insert(where, 'Skill.id="' .. forcedSkill .. '"')
end
table.insert(where, 'Skill.variant = "production"')
table.insert(where, '(Translation.language = "en" OR Translation.language = "' .. lang .. '")')
-- query:
return mw.ext.cargo.query('Skill, Translation', table.concat(fields, ','), {
join = 'Skill.id = Translation.target_id',
where = table.concat(where, ' AND '),
limit = 500
})
end
------------------------------------------------------------------------------------------------------------------------
-- Retrieves the icons and the offered sub-skills from Cargo
------------------------------------------------------------------------------------------------------------------------
local function queryIconAndOffers(forcedSkill)
-- fields:
local fields = {}
table.insert(fields, 'SkillLevel.skill_id = id');
table.insert(fields, 'SkillLevel.level = level');
table.insert(fields, 'SkillLevel.icon = icon');
table.insert(fields, 'SkillLevel.offered_sub_skills = offered_sub_skills');
-- where:
local where = {}
if forcedSkill then
table.insert(where, 'SkillLevel.skill_id="' .. forcedSkill .. '"')
end
table.insert(where, 'SkillLevel.skill_id NOT LIKE "arena%"')
table.insert(where, 'SkillLevel.skill_id NOT LIKE "campaign%"')
-- query:
return mw.ext.cargo.query('SkillLevel', table.concat(fields, ','), {
where = table.concat(where, ' AND '),
limit = 200
})
end
------------------------------------------------------------------------------------------------------------------------
-- Retrieves the most important data from Cargo
------------------------------------------------------------------------------------------------------------------------
local function querySubs(lang, forcedSkill)
-- fields:
local fields = {}
table.insert(fields, 'SubSkill.id = id');
table.insert(fields, 'SubSkill.parent_skill_id = parent_skill_id');
table.insert(fields, 'SubSkill.icon = icon');
table.insert(fields, 'Translation.name = name');
table.insert(fields, 'Translation.description = description');
table.insert(fields, 'Translation.language = language');
-- where:
local where = {}
if forcedSkill then
table.insert(where, 'SubSkill.parent_skill_id="' .. forcedSkill .. '"')
end
table.insert(where, 'SubSkill.variant = "production"')
table.insert(where, '(Translation.language = "en" OR Translation.language = "' .. lang .. '")')
-- query:
return mw.ext.cargo.query('SubSkill, Translation', table.concat(fields, ','), {
join = 'SubSkill.id = Translation.target_id',
where = table.concat(where, ' AND '),
limit = 1000
})
end
------------------------------------------------------------------------------------------------------------------------
-- Folds various skill results from Cargo into a single table
------------------------------------------------------------------------------------------------------------------------
local function consolidateSkills(main, iconAndOffers, subs, lang)
local hub = {}
for _, row in ipairs(main) do
local id = row.id
local variant = row.variant
hub[id] = hub[id] or { id = id, ranks = {} }
local entry = hub[id]
if variant then
-- Level 1, 2 or 3
local i = tonumber(variant) or 1
entry.ranks[i] = entry.ranks[i] or {}
local rank = entry.ranks[i]
if row.language == lang then
rank.name = row.name or ''
rank.description = row.description or ''
end
if row.language == 'en' then
rank.nameEn = row.name or ''
rank.descriptionEn = row.description or ''
end
else
-- Root level (the group/family)
if row.language == lang then
entry.name = row.name or ''
end
if row.language == 'en' then
entry.nameEn = row.name or ''
end
end
end
for _, row in ipairs(iconAndOffers) do
local id = row.id
local entry = hub[id] or { ranks = {} }
local rank = entry.ranks[tonumber(row.level)] or {}
rank.icon = row.icon or ''
local csv = row.offered_sub_skills
local ids = csv and mw.text.split(csv, ',') or {}
rank.subSkills = {}
for _, subId in ipairs(ids) do
table.insert(rank.subSkills, {
id = subId
})
end
end
for _, row in ipairs(subs) do
local id = row.id
local parentId = row.parent_skill_id
local entry = hub[parentId] or { ranks = {} }
for _, rank in ipairs(entry.ranks) do
local subSkills = rank.subSkills or {}
for _, subSkill in ipairs(subSkills) do
if subSkill.id == id then
if row.language == lang then
subSkill.icon = row.icon or ''
subSkill.name = row.name or ''
subSkill.description = row.description or ''
end
if row.language == 'en' then
subSkill.descriptionEn = row.description or ''
end
end
end
end
end
return hub
end
------------------------------------------------------------------------------------------------------------------------
-- Converts a dictionary into a linear array
------------------------------------------------------------------------------------------------------------------------
local function flatten(hub)
local list = {}
for _, entry in pairs(hub) do
list[#list + 1] = entry
end
return list
end
------------------------------------------------------------------------------------------------------------------------
-- Converts an array into a dictionary, where the array.value becomes dic.key and array.index becomes dic.value
------------------------------------------------------------------------------------------------------------------------
local function flipArray(array)
local dic = {}
for index, value in ipairs(array) do
dic[value] = index
end
return dic
end
local ID_TO_ORDER = flipArray(ORDER)
------------------------------------------------------------------------------------------------------------------------
-- Decides the order between two skills.
------------------------------------------------------------------------------------------------------------------------
local function compareSkills(a, b)
local rankA = ID_TO_ORDER[a.id] or 99
local rankB = ID_TO_ORDER[b.id] or 99
if rankA ~= rankB then
return rankA < rankB
end
return a.name < b.name
end
------------------------------------------------------------------------------------------------------------------------
-- Moves punctuation outside the quotes
------------------------------------------------------------------------------------------------------------------------
local function repairPunctuation(text)
local allQuotes = [=["„“”‘’«»「」『』]=]
local punctuation = "%,%.%!%?"
local pattern = "([" .. punctuation .. "])([" .. allQuotes .. "])"
text = mw.ustring.gsub(text, pattern, "%2%1")
return text
end
------------------------------------------------------------------------------------------------------------------------
-- Returns a "Foo/XX" string based on the first quoted word in a text, if any.
------------------------------------------------------------------------------------------------------------------------
local function findUrl(descriptionInEnglish, lang)
local name = descriptionInEnglish:match("“(.-)”")
if name and name ~= '' then
return getUrl(name, lang)
end
return nil
end
------------------------------------------------------------------------------------------------------------------------
-- Converts quoted text into wiki links
------------------------------------------------------------------------------------------------------------------------
local function replaceQuotesWithLinks(text, lang, url)
local allQuotes = [=["„“”«»「」『』]=]
local pattern = "([" .. allQuotes .. "])([^" .. allQuotes .. "]+)([" .. allQuotes .. "])"
text = mw.ustring.gsub(text, pattern, "%1[[" .. url .. "|%2]]%3")
local quotePairs = {
{ '‘', '’' }, -- English "smart" single quotes
}
if lang == 'ko' then
table.insert(quotePairs, { "'", "'" }) -- we're checking for single quotes only in Korean
end
for _, pair in ipairs(quotePairs) do
local openQuote = pair[1]
local closeQuote = pair[2]
local pairedPattern = openQuote .. "([^" .. closeQuote .. "]+)" .. closeQuote
text = mw.ustring.gsub(text, pairedPattern, "[[%1]]")
end
return text
end
------------------------------------------------------------------------------------------------------------------------
-- Converts the first capitalized mid-sentence words into wiki links
------------------------------------------------------------------------------------------------------------------------
local function replaceCapitalizedWithLinks(text, url)
text = mw.ustring.gsub(text, "([^%.]) (%u[%w ’]+)", "%1 [[" .. url .. "|%2]]")
return text
end
------------------------------------------------------------------------------------------------------------------------
-- Adds [[link]] inside a description
------------------------------------------------------------------------------------------------------------------------
local function linkifyDescription(description, descriptionEn, lang)
descriptionEn = repairPunctuation(descriptionEn)
local url = findUrl(descriptionEn, lang)
if not url then return description end
description = lang == 'en' and descriptionEn or repairPunctuation(description)
local adapted = replaceQuotesWithLinks(description, lang, url)
if description ~= adapted then
description = adapted
else
description = replaceCapitalizedWithLinks(description, url)
end
return description
end
------------------------------------------------------------------------------------------------------------------------
-- Adds [[links]] inside descriptions
------------------------------------------------------------------------------------------------------------------------
local function linkifyDescriptions(skills, lang)
for _, group in ipairs(skills) do
local ranks = group.ranks or {}
for _, rank in ipairs(ranks) do
local subSkills = rank.subSkills or {}
for _, subSkill in pairs(subSkills) do
if subSkill.description then
subSkill.description = linkifyDescription(subSkill.description, subSkill.descriptionEn, lang)
end
end
end
end
end
------------------------------------------------------------------------------------------------------------------------
-- Displays a single skill (icon+name+description)
------------------------------------------------------------------------------------------------------------------------
local function createSkill(target, class, size)
local root = mw.html.create('div'):addClass('box'):addClass(class)
root:tag('div'):addClass('icon-container'):wikitext('[[File:' .. target.icon .. '.png|' .. size .. 'px]]')
local textContainer = root:tag('div'):addClass('text-container')
textContainer:tag('div'):addClass('box-name'):wikitext('<b>' .. target.name .. '</b>')
textContainer:tag('div'):addClass('box-description'):wikitext(target.description)
return root
end
------------------------------------------------------------------------------------------------------------------------
-- Displays the main content
------------------------------------------------------------------------------------------------------------------------
local function renderSkills(skills, words, lang)
local root = mw.html.create()
local currentCategory = nil
local isAll = #skills > 1
for _, group in ipairs(skills) do
local id = group.id
local category = CATEGORIES[id]
if category and category ~= currentCategory then
currentCategory = category
if isAll then
root:tag('h2'):addClass('category'):wikitext(words[category])
end
end
local ranks = group.ranks or {}
if isAll then
local url = getUrl(group.nameEn, lang)
root:tag('h3'):addClass('group'):wikitext('[[' .. url .. '|' .. group.name .. ']]')
end
for _, rank in ipairs(ranks) do
root:node(createSkill(rank, 'rank', 64))
local subSkills = rank.subSkills or {}
for _, subSkill in pairs(subSkills) do
root:node(createSkill(subSkill, 'sub', 32))
end
end
end
return tostring(root)
end
------------------------------------------------------------------------------------------------------------------------
-- Bundles together all cargo skill queries:
-- Also called by Module:SkillSynergies
-- Also called by Module:SkillArtifacts
------------------------------------------------------------------------------------------------------------------------
function p.getSkillsFromCargo(lang, skill)
local main = queryMain(lang, skill)
--if 1 then return main end
local iconAndOffers = queryIconAndOffers(skill)
local subs = querySubs(lang, skill)
local hub = consolidateSkills(main, iconAndOffers, subs, lang)
return hub
end
------------------------------------------------------------------------------------------------------------------------
-- Main public function
------------------------------------------------------------------------------------------------------------------------
function p.display(frame)
--if 1 then return dump(frame) end
-- Args
local args = frame.args
local lang = args.lang or 'en'
local skill = args.skill
--lang = 'fr'
--skill = 'skill_formation'
-- Language
local words = translateIds(TRANSLATION_IDS, lang)
-- Cargo
local skillsHub = p.getSkillsFromCargo(lang, skill)
local skills = flatten(skillsHub)
--if 1 then return dump(#skills) end
if #skills == 0 then
return 'No skills found!'
end
-- Various manipulations
table.sort(skills, compareSkills)
linkifyDescriptions(skills, lang)
-- Content
local markup = renderSkills(skills, words, lang)
--if 1 then return dump(markup) end
-- Output
local styleTag = frame:extensionTag('templatestyles', '', { src = frame:getTitle() .. '/styles.css' })
return styleTag .. markup
end
return p


