Module:Sandbox/Aurelain/ArtifactsOverview

From Heroes of Might and Magic: Olden Era Official Wiki

This module renders a giant table with all artifacts. Scroll artifacts are consolidated into 3 generic scroll artifacts (normal, enchanted and mythic). Campaign artifacts are excluded.

A hyper parameter is supported. For more information, see Module:UtilHyper/doc.

Usage

Input: {{#invoke:Sandbox/Aurelain/ArtifactsOverview|display | lang=en | hyper=Attack=Hero_Basics#Attributes}}

Output:

ArtifactDescriptionUpgradeUpgrade costSlotRaritySet
Fish-to-the-Face+1 Attack+1 more Attack25Main HandICommonpaupers_glory_item_set
RapierFriendly creatures deal +10% Melee Damage+5% more Melee Damage50Main HandIIRareduelists_pride_item_set
Lance+3 Attack+2 more Attack50Main HandIIRareknights_honor_item_set
Ogre's Club of Havoc+4 Attack+2 more Attack. Can be upgraded an unlimited number of times. <super>(+120)</super>Main HandIIRare
Red Dragon Flame Tongue+2 Attack and Defense+1 to both attributes50Main HandIIRarepower_of_the_dragon_father_item_set
Garotte–1 Speed to all enemiesMain HandIIIEpic
Shroomwood BowEliminates Ranged Damage penalty for friendly creaturesFriendly creatures deal +5% Ranged Damage75Main HandIIIEpicliving_arrows_item_set
Truthmaker–10% Attack to enemy creatures–5% more Attack75Main HandIIIEpicrule_of_shadow_item_set
Dark Hatchet+4 Attack, then +15% total Attack+2 Attack75Main HandIIIEpicshadow_of_death_item_set
Shaman Staff+6 Spell Power+3 more Spell Power75Main HandIIIEpicshamaniac_soul_item_set
Sword of Judgement+2 Attack, Defense, Spell Power, and Knowledge+1 more to every attribute100Main HandIVLegendaryangelic_alliance_item_set
Demon Claw+12 Attack, but –4 Defense+4 more Attack100Main HandIVLegendarybeelzebubs_blessing_item_set
ExcaliburHeroic Strike kills +1 unit(s). Works on creatures up to tier 7+1 more unit(s)100Main HandIVLegendarylegendary_hero_item_set
right_hand
BucklerFriendly creatures can counterattack even if the attacking enemy has “Swift Strike.”Friendly creatures deal +5% Melee Damage25Off HandICommonduelists_pride_item_set
Singing Pan-PipeAll effects applied by friendly creatures last +1 round(s)+1 more round(s)25Off HandICommoninner_song_item_set
Misericorde+2 Attack+1 more Attack25Off HandICommonknights_honor_item_set
Shoddy Shield+2 DefenseOff HandICommon
CaduceusHeroic Strike restores 4 mana+4 more mana50Off HandIIRarelegendary_hero_item_set
Crescent DaggerSpells and creature abilities ignore 40% of enemy magic resistanceIgnore +20% more magic resistance50Off HandIIRareethereal_knowledge_item_set
Fine Wand+1 level to all spellsOff HandIIRare
Pelagic HourglassThe cooldowns of all spells are reduced by 1 round(s)Off HandIIRare
Shield of the Rampaging Ogre+4 Defense+2 more Defense. Can be upgraded an unlimited number of times. <super>(+120)</super>Off HandIIRare
Dragon Scale Shield+3 Attack and Defense+1 to both attributes75Off HandIIIEpicpower_of_the_dragon_father_item_set
Truthseeker–10% Defense to enemy creatures–5% more Defense75Off HandIIIEpicrule_of_shadow_item_set
Lion's Shield of Courage+2 Attack, Defense, Spell Power, and Knowledge+1 more to every attribute100Off HandIVLegendaryangelic_alliance_item_set
Chitinous Shield+12 Defense, but –4 Attack+4 more Defense100Off HandIVLegendarybeelzebubs_blessing_item_set
armor
Chain Mail+3 DefenseArmorICommon
Rags+1 Spell Power+1 more Spell Power25ArmorICommonpaupers_glory_item_set
Eagle Armor+2 Morale and LuckArmorIIRare
Vortex RobeWhenever the opponent casts a spell, restores 40% of its cost as mana to the wearerRestores 60% of the spell’s cost50ArmorIIRareethereal_knowledge_item_set
Plate Armor+3 Defense+2 more Defense50ArmorIIRareknights_honor_item_set
Tunic of the Cyclops King+4 Spell Power+2 more Spell Power. Can be upgraded an unlimited number of times. <super>(+120)</super>ArmorIIRare
Robe of EnlightenmentMakes all tier-3 spells available to the heroArmorIIIEpic
Ribcage Armor+4 Defense, then +15% total Defense+2 more Defense75ArmorIIIEpicshadow_of_death_item_set
Iridescent Cloak+6 Knowledge+3 more Knowledge75ArmorIIIEpicshamaniac_soul_item_set
Armor of Wonder+2 Attack, Defense, Spell Power, and Knowledge+1 more to every attribute100ArmorIVLegendaryangelic_alliance_item_set
Heartbeat+12 Spell Power, but –4 Knowledge+4 more Spell Power100ArmorIVLegendarybeelzebubs_blessing_item_set
Dragon Scale Armor+4 Attack and Defense+1 to both attributes100ArmorIVLegendarypower_of_the_dragon_father_item_set
head
Head Torch+1 sight radiusHeadICommon
Fancy MaskGenerates 2 Focus Point(s) at the start of the battle+2 more Focus Point(s)25HeadICommoninner_song_item_set
Armet+2 Defense+1 more Defense25HeadICommonknights_honor_item_set
Straw Hat+1 Knowledge+1 more Knowledge25HeadICommonpaupers_glory_item_set
Crown of the Supreme Magi+4 Knowledge+2 more Knowledge. Can be upgraded an unlimited number of times. <super>(+120)</super>HeadIIRare
Scholar's Tiara+2 Knowledge+1 more Knowledge50HeadIIRarescholars_wisdom_item_set
Mirror TiaraRestores 8 mana to the hero each morning+8 more mana75HeadIIIEpictranquility_item_set
Helm of Heavenly Enlightenment+2 Attack, Defense, Spell Power, and Knowledge+1 more to every attribute100HeadIVLegendaryangelic_alliance_item_set
Beelzebub's Boon+12 Knowledge, but –4 Spell Power+4 more Knowledge100HeadIVLegendarybeelzebubs_blessing_item_set
Seven Eyes+1 level(s) to all spells while worn+1 more spell level(s)100HeadIVLegendaryethereal_knowledge_item_set
Crown of Dragontooth+4 Spell Power and Knowledge+1 to both attributes100HeadIVLegendarypower_of_the_dragon_father_item_set
Amelchia's Law–1 Initiative to enemy creatures–1 more Initiative100HeadIVLegendaryrule_of_shadow_item_set
Sirin MaskThe cooldowns of all spells are reduced by 1 round(s)–1 fewer round(s)100HeadIVLegendaryshamaniac_soul_item_set
Spellbinder's HatMakes all tier-5 spells available to the heroHeadIVLegendary
Two-faced MaskThe hero can use the Spellbook 1 additional time(s) per battle roundHeadIVLegendary
boots
Magic SlippersSpells cost 2 less mana1 less mana25BootsICommonethereal_knowledge_item_set
Dragonbone Greaves+1 Spell Power and Knowledge+1 to both attributes25BootsICommonpower_of_the_dragon_father_item_set
Boots of Travel+10 Movement points+5 more Movement points50BootsIIRarewanderers_way_item_set
Legion's Leg+50% to all creature growth in all your citiesBootsIIIEpic
Bone Boots+4 Spell Power, then +15% total Spell Power+2 more Spell Power75BootsIIIEpicshadow_of_death_item_set
Swamp BootsEliminates all terrain penaltiesBootsIIIEpic
Sandals of the Saint+2 Attack, Defense, Spell Power, and Knowledge+1 more to every attribute100BootsIVLegendaryangelic_alliance_item_set
Seven-League Boots+30 Movement pointsBootsIVLegendary
Warlord Boots+2 Initiative to friendly creaturesBootsIVLegendary
belt
Rope Belt+1 Defense+1 more Defense25BeltICommonpaupers_glory_item_set
Soulless Sash–1 Morale to enemy creaturesBeltICommon
Flask of OblivionAll negative effects on friendly creatures last for –1 round(s)–1 fewer round(s)50BeltIIRareelixir_of_life_item_set
Endless BagProduces +1 Gem, Crystal, or Mercury daily. (The resource depends on the bearer’s faction.)BeltIIRare
Warrior's Belt+2 Attack+1 more Attack50BeltIIRarewarriors_strength_item_set
Protective Belt–10% Magic Damage taken. Friendly creatures are immune to the effects of “Sleep.”–5% less Magic Damage taken75BeltIIIEpicgifts_of_dwarven_lords_item_set
Skull of Milos+1000 Gold daily+1000 more Gold75BeltIIIEpicmilos_curse_item_set
Slithering Sash+3 Spell Power and Knowledge+1 to both attributes75BeltIIIEpicpower_of_the_dragon_father_item_set
Celestial Sash of Bliss+2 Attack, Defense, Spell Power, and Knowledge+1 more to every attribute100BeltIVLegendaryangelic_alliance_item_set
Magic Key RingNegates all effects that restrict creature rearrangement, surrendering, fleeing, and using the Spellbook and hero abilities in battleBeltIVLegendary
Spells in BottlesMakes all tier-4 spells available to the heroBeltIVLegendary
back
Omencaller–1 to enemy LuckBackICommon
Automated Antimagic Shield–10% Magic Damage taken. Friendly creatures are immune to the effects of “Berserk.”–5% less Magic Damage taken50BackIIRaregifts_of_dwarven_lords_item_set
Monster HeadAll creatures always have neutral Morale in battleBackIIRare
Dragon Wing Tabard+2 Spell Power and Knowledge+1 to both attributes50BackIIRarepower_of_the_dragon_father_item_set
BackpackThe hero has +10 Movement points+5 more Movement points50BackIIRarewanderers_way_item_set
Wizard's Cloak+2 Spell Power+1 more Spell Power50BackIIRarewizards_might_item_set
Banner of Six Winds+1 Speed to friendly creaturesBackIIIEpic
Light-and-Shade CloakFriendly creatures can still use Ranged attacks while adjacent to enemy creaturesFriendly creatures deal +5% Ranged Damage75BackIIIEpicliving_arrows_item_set
Second Shade+4 Knowledge, then +15% total Knowledge+2 more Knowledge75BackIIIEpicshadow_of_death_item_set
Ambassador's Sash+10% to Persuasion Power in Diplomacy+5% more Persuasion Power in Diplomacy100BackIVLegendaryambassadors_word_item_set
Fallen Angel WingsGrants constant flightBackIVLegendary
ring
Wooden Ring+1 Morale+1 more Morale25RingsICommonpaupers_glory_item_set
Quiet Eye of a Dragon+1 Attack and Defense+1 to both attributes25RingsICommonpower_of_the_dragon_father_item_set
Clenching RingAll effects applied by the hero last +1 round(s)+1 more round(s)25RingsICommonshamaniac_soul_item_set
Chain LinkForbids the wearer and the enemy hero to cast tier-1 and 2 spells in combatRingsIIRare
Ring of LifeAll positive effects on friendly creatures last for +1 round(s)+1 more round(s)50RingsIIRareelixir_of_life_item_set
Emerald Resonance Controller–10% to enemy hero Spell Power. Friendly creatures are immune to the effects that kill additional units–5% more50RingsIIRaregifts_of_dwarven_lords_item_set
Keeper's Ring+2 Defense+1 more Defense50RingsIIRarekeepers_fortitude_item_set
Ring of NeutralityAll creatures always have neutral Luck in battleRingsIIRare
Sixth Finger–1 to enemy Morale and LuckRingsIIRare
Shard of SerenityRestores 6 mana to the hero each morning+6 more mana50RingsIIRaretranquility_item_set
Tabar Signet-Ring+1 Luck+1 more Luck50RingsIIRareukhtabar_seal_item_set
Brass KnucklesFriendly creatures take –30% counterattack Damage–15% less counterattack Damage75RingsIIIEpicduelists_pride_item_set
Crimson Resonance Controller–20% to enemy hero Spell Power. Friendly creatures are immune to effects that prevent them from attacking–10% more to enemy Spell Power75RingsIIIEpicgifts_of_dwarven_lords_item_set
Lord's RingFriendly creatures have +1 InitiativeRingsIIIEpic
Seal of Silence+1 round(s) to the cooldown of all enemy spellsRingsIIIEpic
Soulcaller RingThe enemy loses 1 levels on all Magic School spellsRingsIVLegendary
item_slot
Boreoloth's Tail+1 Defense+1 more Defense. Can be upgraded an unlimited number of times. <super>(+30)</super>ItemICommon
Boreoloth's Hand+1 Attack+1 more Attack. Can be upgraded an unlimited number of times. <super>(+30)</super>ItemICommon
Boreoloth's Head+1 Knowledge+1 more Knowledge. Can be upgraded an unlimited number of times. <super>(+30)</super>ItemICommon
Boreoloth's Heart+1 Spell Power+1 more Spell Power. Can be upgraded an unlimited number of times. <super>(+30)</super>ItemICommon
Music SheetFriendly creatures’ abilities deal +40% Damage+20% more Damage25ItemICommoninner_song_item_set
Magic ScrollAdds the “Impending Fate” spell to the hero’s SpellbookItemICommon
Golden Moth+250 Gold daily+250 more Gold25ItemICommonmilos_curse_item_set
Last Coin+1 Luck+1 more Luck25ItemICommonpaupers_glory_item_set
Liquid Silence+1 round(s) to the cooldown of all enemy creature abilities+1 more round(s)25ItemICommonrule_of_shadow_item_set
Magic MirrorRestores 4 mana to the hero each morning+4 more mana25ItemICommontranquility_item_set
Ancient IdolThe hero ignores the restriction on using the same Magic School twiceItemIIRare
Deck of SpellsMakes all tier-1 spells available to the heroItemIIRare
Enchanted Magic ScrollAdds the max. level “Impending Fate” spell to the hero’s SpellbookItemIIRare
Keeper's Amulet+2 Defense+1 more Defense50ItemIIRarekeepers_fortitude_item_set
Quivering QuiverFriendly creatures deal +10% Ranged Damage+5% more Ranged Damage50ItemIIRareliving_arrows_item_set
Golden Pig+500 Gold daily+500 more Gold50ItemIIRaremilos_curse_item_set
Orb of DestructionHeroic Strike deals +10 base DamageItemIIRare
Scholar's Amulet+2 Knowledge+1 more Knowledge50ItemIIRarescholars_wisdom_item_set
Spyglass+2 sight radiusItemIIRare
Ukh Signet+1 Morale+1 more Morale50ItemIIRareukhtabar_seal_item_set
Voodoo DollWhen the wearer is defeated, they can still be hired back in the TavernItemIIRare
Warrior's Amulet+2 Attack+1 more Attack50ItemIIRarewarriors_strength_item_set
Wizard's Amulet+2 Spell Power+1 more Spell Power50ItemIIRarewizards_might_item_set
Flag of TruceThe wearer keeps their army whenever they flee from battleItemIIIEpic
Golden Goose EggGrants a random wondrous reward after a random number of upgradesVaries <super>(60)</super>ItemIIIEpic
Drum of WarFriendly creatures deal +1 Damage+1 more Damage50ItemIIIEpicknights_honor_item_set
Mythic Magic ScrollAdds the “Town Portal” spell to the hero’s SpellbookItemIIIEpic
Orb of InhibitionOn the first round of the battle, heroes cannot use their Spellbook or active abilitiesItemIIIEpic
Orb of DawnAll of the hero’s Daylight spells have +1 level(s). Enemy hero has –1 Daylight spell levelItemIIIEpic
Orb of EternityAll of the hero’s Arcane spells have +1 level(s). Enemy hero has –1 Arcane spell levelItemIIIEpic
Orb of TwilightAll of the hero’s Nightshade spells have +1 level(s). Enemy hero has –1 Nightshade spell levelItemIIIEpic
Primordial OrbAll of the hero’s Primal spells have +1 level(s). Enemy hero has –1 Primal spell levelItemIIIEpic
Runestone ShardsMakes all tier-2 spells available to the heroItemIIIEpic
Shackles of WarPrevents surrender and flee in battle. (This affects both the wearer and their opponent.)ItemIIIEpic
Tactical GuideThe opponent cannot rearrange creatures before the start of battleItemIIIEpic
Diplomatic Gifts+10% to Persuasion Power in Diplomacy+5% more Persuasion Power in Diplomacy100ItemIVLegendaryambassadors_word_item_set
Daylight TomeAllows the hero to cast all Daylight spellsItemIVLegendary
Nightshade TomeAllows the hero to cast all Nightshade spellsItemIVLegendary
Primal TomeAllows the hero to cast all Primal spellsItemIVLegendary
Arcane TomeAllows the hero to cast all Arcane spellsItemIVLegendary
Demonic HeartReplaces all the hero’s spells with their Masterful versions (if they exist)ItemIVLegendary
Lifeblood FaeFriendly creatures have +2 HP+1 more HP75ItemIVLegendaryelixir_of_life_item_set
Pole StarGlobal Map spells of the hero cost 0 manaItemIVLegendary
unic_slot
Holy Sigil of EridoreWhile worn, grants all unlearned Subskills of the following Skills: Logistics, Battle Magic, WisdomRelicIIIEpic
Holy Sigil of InsaraWhile worn, grants all unlearned Subskills of the following Skills: Leadership, Insight, Diplomacy, TacticsRelicIIIEpic
Holy Sigil of MeareaWhile worn, grants all unlearned Subskills of the following Skills: Luck, Economy, Daylight MagicRelicIIIEpic
Holy Sigil of QuixWhile worn, grants all unlearned Subskills of the following Skills: Arcane Magic, Scouting, Battlecraft, ThaumaturgyRelicIIIEpic
Holy Sigil of RophWhile worn, grants all unlearned Subskills of the following Skills: Righteousness, Necromancy, Murmuring, Triumvirate’s Strength, Abyssal Communion, Summon SwarmRelicIIIEpic
Holy Sigil of The Second ManWhile worn, grants all unlearned Subskills of the following Skills: Offense, Summon Avatar, Siegecraft, CombatRelicIIIEpic
Holy Sigil of The Seven MagiWhile worn, grants all unlearned Subskills of the following Skills: Sorcery, Nightshade Magic, ResistanceRelicIIIEpic
Holy Sigil of UurdtWhile worn, grants all unlearned Subskills of the following Skills: Defense, Primal Magic, RecruitmentRelicIIIEpic

-- Usage: see Module:ArtifactsOverview/doc
local p = {}
local f = mw.ustring.format

local SLOT_ORDER = {
    left_hand = 1,  -- Main Hand
    right_hand = 2, -- Off Hand
    armor = 3,      -- Armor
    head = 4,       -- Head
    boots = 5,      -- Boots
    belt = 6,       -- Belt
    back = 7,       -- Back
    ring = 8,       -- Rings
    item_slot = 9,  -- Item
    unic_slot = 10, -- Relic
}

local RARITY_ORDER = {
    common = 1,
    rare = 2,
    epic = 3,
    legendary = 4,
}

local SLOT_ICONS = {
    armor = 'Icon_Equipment_Armor.png',
    back = 'Icon_Equipment_Cloak.png',
    belt = 'Icon_Equipment_Belt.png',
    boots = 'Icon_Equipment_Boots.png',
    head = 'Icon_Equipment_Helmet.png',
    item_slot = 'Icon_Equipment_Sundries.png',
    left_hand = 'Icon_Equipment_RightHand.png',
    right_hand = 'Icon_Equipment_LeftHand.png',
    ring = 'Icon_Equipment_Rings.png',
    unic_slot = 'Icon_Equipment_Banner.png',
}

-- Note: The right side is just a fallback, it will seldom be used.
local TRANSLATION_IDS = {
    wiki_none = 'None',
    wiki_ao_artifact = 'Artifact',
    unit_window_narrative = 'Description',
    wiki_ao_upgrade = 'Upgrade', -- inventoryUpgradeItemLabel
    wiki_ao_slot = 'Slot',
    wiki_ao_rarity = 'Rarity',
    wiki_ao_set = 'Set',
    wiki_ao_upgrade_cost = 'Upgrade cost',
    wiki_ao_armor = 'Armor',
    wiki_ao_back = 'Back',
    wiki_ao_belt = 'Belt',
    wiki_ao_boots = 'Boots',
    wiki_ao_head = 'Head',
    wiki_ao_item_slot = 'Item',
    wiki_ao_left_hand = 'Main Hand',
    wiki_ao_right_hand = 'Off Hand',
    wiki_ao_ring = 'Rings',
    wiki_ao_unic_slot = 'Relic',
    wiki_ao_common = 'Common',
    wiki_ao_rare = 'Rare',
    wiki_ao_epic = 'Epic',
    wiki_ao_legendary = 'Legendary',
    wiki_ao_special = 'Special',
}

local ROMAN = { 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII' }

-- Displays 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)
    if not nameEn then
        return ''
    end
    local url = 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 .. '🗯️'
    end
    for _, row in ipairs(results) do
        local name = row['name']
        if name and name ~= '' then
            dictionary[row['target_id']] = name
        end
    end
    return dictionary
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

-- Parses and runs a Cargo query from wiki-like syntax
------------------------------------------------------------------------------------------------------------------------
local function runQuery(query, ...)
    query = string.gsub(query, "%-%-.-\n", "") -- remove "comments"
    query = select('#', ...) > 0 and mw.ustring.format(query, ...) or query
    query = mw.text.trim(query)
    local tables = ''
    local fields = ''
    local options = {}
    for part in query:gmatch('[^|]+') do
        local key, val = part:match('^%s*([^=]+)%s*=%s*(.-)%s*$')
        if key and val then
            key = mw.text.trim(key):lower()
            val = mw.text.trim(val)
            val = val:gsub('%s', ' ')
            if key == 'tables' then
                tables = val
            elseif key == 'fields' then
                fields = val
            elseif key == 'limit' then
                options[key] = tonumber(val) or val
            elseif key == 'join on' then
                options['join'] = val
            else
                options[key] = val
            end
        end
    end
    return mw.ext.cargo.query(tables, fields, options)
end

-- Interrogates the main Cargo table
------------------------------------------------------------------------------------------------------------------------
local function queryMain(lang)
    return runQuery([[
        tables =
            Artifact = A,
            Translation = T,
            Translation = E
        | join on =
            A.id = T.target_id,
            A.id = E.target_id
        | fields =
            A.id = id,
            A.slot = slot,
            A.rarity = rarity,
            A.artifact_set_id = artifact_set_id,
            A.max_level = max_level,
            A.cost_base = cost_base,
            A.cost_per_level = cost_per_level,
            T.language = language,
            T.name = name,
            T.description = description,
            T.type = translation_type,
            E.name = nameEn,
        | where =
            A.id NOT LIKE 'campaign_%%'
            AND T.language = '%s'
            AND E.type = 'artifact'
            AND E.language = 'en'
        | limit =
            1000
    ]], lang, lang, lang);
    --table.insert(where, '(A.id = "enchanted_magic_scroll_artifact_day_10_magic_second_song" or A.id="enchanted_magic_scroll_artifact_day_13_magic_holy_arms")')
end

-- Folds magic scrolls into 3 artifacts (normal, enchanted and mythic):
------------------------------------------------------------------------------------------------------------------------
local function consolidateScrolls(lang, results)
    local hub = {}
    for _, row in ipairs(results) do
        local id = row.id
        if id:find('magic_scroll') then
            if id:find('mythic') then
                id = 'mythic_scroll_box_artifact'
            else
                if id:find('enchanted') then
                    id = 'enchanted_magic_scroll_artifact'
                else
                    id = 'magic_scroll_artifact'
                end
            end
            local entry = hub[id]
            if not entry then
                entry = {}
                entry.id = id or ''
                entry.name = row.name or ''
                entry.nameEn = row.nameEn or ''
                entry.spells = {}
                entry.slot = row.slot or ''
                entry.rarity = row.rarity or ''
                entry.artifact_set_id = row.artifact_set_id or ''
                entry.max_level = 0
                entry.cost_base = 0
                entry.cost_per_level = 0
                entry.description = '' -- will be overridden below
                entry.upgrade = ''
                entry.narrative = ''   -- will be overridden below
                hub[id] = entry
            end
            if row.translation_type == 'artifact' then
                local spellId = row.id:gsub('.*artifact_', '')
                table.insert(entry.spells, spellId)
                entry.description = row.description or ''
            end
            if row.translation_type == 'artifact_narrative' then
                entry.narrative = row.description or ''
            end
        end
    end
    return hub
end

-- Folds all Cargo results into a single dictionary:
------------------------------------------------------------------------------------------------------------------------
local function consolidate(lang, results)
    local hub = consolidateScrolls(lang, results)
    for _, row in ipairs(results) do
        local id = row.id
        if not id:find('magic_scroll') then
            local entry = hub[id]
            if not entry then
                entry = {}
                entry.id = id or ''
                entry.name = row.name or ''
                entry.nameEn = row.nameEn or ''
                entry.spells = {}
                entry.slot = row.slot or ''
                entry.rarity = row.rarity or ''
                entry.artifact_set_id = row.artifact_set_id or ''
                entry.max_level = tonumber(row.max_level) or 0
                entry.cost_base = tonumber(row.cost_base) or 0
                entry.cost_per_level = tonumber(row.cost_per_level) or 0
                entry.description = '' -- will be overridden below
                entry.upgrade = ''     -- will be overridden below
                entry.narrative = ''   -- will be overridden below
                hub[id] = entry
            end
            if row.translation_type == 'artifact' then
                entry.description = row.description or ''
            end
            if row.translation_type == 'artifact_upgrade' then
                entry.upgrade = row.description or ''
            end
            if row.translation_type == 'artifact_narrative' then
                entry.narrative = row.description or ''
            end
        end
    end
    return hub
end

------------------------------------------------------------------------------------------------------------------------
local function sortList(a, b)
    local slotA = SLOT_ORDER[a.slot] or 99
    local slotB = SLOT_ORDER[b.slot] or 99
    if slotA ~= slotB then
        return slotA < slotB
    end

    local rarityA = RARITY_ORDER[a.rarity] or 99
    local rarityB = RARITY_ORDER[b.rarity] or 99
    if rarityA ~= rarityB then
        return rarityA < rarityB
    end

    return a.id < b.id
end

-- Boilerplate for the header cells
------------------------------------------------------------------------------------------------------------------------
local function addTh(tr, text)
    return tr:tag('th'):wikitext(text)
end

-- Boilerplate for the body cells
------------------------------------------------------------------------------------------------------------------------
local function addTd(tr, content)
    if type(content) == "string" then
        return tr:tag('td'):wikitext(content)
    else
        return tr:tag('td'):node(content)
    end
end

-- Boilerplate for the separator rows
------------------------------------------------------------------------------------------------------------------------
local function addSeparator(htmlTable, className, content)
    htmlTable:tag('tr')
        :addClass('separator')
        :addClass(className)
        :tag('td'):attr('data-sort-value', ''):attr('colspan', 100):wikitext(content):done()
end

-- TABLE HEADER
------------------------------------------------------------------------------------------------------------------------
local function createHeader(htmlTable, words)
    local tr = htmlTable:tag('tr')
    --addTh(tr, 'id')
    addTh(tr, words.wiki_ao_artifact):attr('colspan', 2)
    addTh(tr, words.unit_window_narrative)
    addTh(tr, words.wiki_ao_upgrade)
    addTh(tr, words.wiki_ao_upgrade_cost)
    addTh(tr, words.wiki_ao_slot)
    addTh(tr, words.wiki_ao_rarity)
    addTh(tr, words.wiki_ao_set)
    --addTh(tr, 'narrative')
end

------------------------------------------------------------------------------------------------------------------------
local function renderIcon(id, nameEn, lang)
    local url = getUrl(nameEn, lang)
    return f('[[File:%s.png|64px|link=%s]]', id, url)
end

------------------------------------------------------------------------------------------------------------------------
local function renderName(name, nameEn, lang)
    local url = getUrl(nameEn, lang)
    return f('[[%s|%s]]', url, name)
end

------------------------------------------------------------------------------------------------------------------------
local function renderDescription(description)
    description = mw.ustring.gsub(description, '[.。]-$', '') -- remove the ending dot
    -- TODO: add links
    return mw.text.trim(description)
end

------------------------------------------------------------------------------------------------------------------------
local function renderUpgrade(item, words, lang)
    if item.max_level < 2 then
        return ''
    end
    if item.id:find('goose_egg') then
        local url = getUrl('Golden_Goose_Egg', lang)
        return f('[[%s|%s]]', url, words.wiki_ao_special)
    end
    local text = item.upgrade
    text = mw.ustring.gsub(text, '^.-[::]', '') -- remove the "Upgrade: " prefix
    if item.max_level < 900 then
        text = mw.ustring.gsub(text, '[.。]-$', '') -- remove the ending dot
    end
    --if item.max_level > 900 then
    --text = mw.ustring.gsub(text, '[.。].-$', '') -- remove the last sentence which should say "Can be upgraded..."
    --end
    return mw.text.trim(text)
end

------------------------------------------------------------------------------------------------------------------------
local function addUpgradeCost(tr, item)
    if item.max_level > 900 then
        local symbol = item.cost_per_level == 0 and '' or '+'
        local nr = item.cost_per_level == 0 and item.cost_base or item.cost_base + item.cost_per_level
        local text = f('<span class="infinity">∞</span> <super>(%s%s)</super>', symbol, nr)
        tr:tag('td'):wikitext(text):attr('data-sort-value', 1000 + item.cost_per_level)
    elseif item.max_level < 2 then
        tr:tag('td'):attr('data-sort-value', 0)
    else
        local cost = item.cost_base + item.cost_per_level
        tr:tag('td'):wikitext(cost):attr('data-sort-value', cost)
    end
end

------------------------------------------------------------------------------------------------------------------------
local function renderSlot(item, words, frame)
    local file = f('[[File:%s|48px|link=]]', SLOT_ICONS[item.slot])
    local hint = words['wiki_ao_' .. item.slot]
    return frame:preprocess(f('{{Hint|%s|%s}}', file, hint))
end

------------------------------------------------------------------------------------------------------------------------
local function renderRarity(item, words, frame)
    local text = ROMAN[RARITY_ORDER[item.rarity]]
    local hint = words['wiki_ao_' .. item.rarity]
    return frame:preprocess(f('{{Hint|%s|%s}}', text, hint))
end

-- TABLE BODY
------------------------------------------------------------------------------------------------------------------------
local function createBody(htmlTable, list, lang, words, frame)
    local currentRarity = list[1].rarity
    local currentSlot = list[1].slot
    for _, item in ipairs(list) do
        -- separators
        if item.slot ~= currentSlot then
            currentSlot = item.slot
            currentRarity = item.rarity
            addSeparator(htmlTable, 'separator-large', currentSlot)
        elseif item.rarity ~= currentRarity then
            currentRarity = item.rarity
            addSeparator(htmlTable, 'separator-tiny ' .. currentRarity, '')
        end

        local tr = htmlTable:tag('tr'):addClass('artifact'):addClass(item.rarity)
        --addTd(tr, item.id)
        addTd(tr, renderIcon(item.id, item.nameEn, lang))
        addTd(tr, renderName(item.name, item.nameEn, lang))
        addTd(tr, renderDescription(item.description))
        addTd(tr, renderUpgrade(item, words, lang))
        addUpgradeCost(tr, item)
        addTd(tr, renderSlot(item, words, frame)):attr('data-sort-value', SLOT_ORDER[item.slot])
        addTd(tr, renderRarity(item, words, frame)):attr('data-sort-value', RARITY_ORDER[item.rarity])
        addTd(tr, item.artifact_set_id)
        --addTd(tr, item.narrative)
    end
end

-- M A I N
------------------------------------------------------------------------------------------------------------------------
function p.display(frame)
    --do return dump(frame) end

    -- Args
    local args = frame.args
    local lang = args.lang ~= '' and args.lang or 'en'
    --lang = 'fr'

    -- Language
    local words = translateIds(TRANSLATION_IDS, lang)

    -- Cargo
    local results = queryMain(lang)
    local hub = consolidate(lang, results)
    local list = flatten(hub);
    if #list == 0 then return words.wiki_none end

    -- Various manipulations
    table.sort(list, sortList)

    -- Table
    local htmlTable = mw.html.create('table')
    htmlTable:addClass('wikitable sortable table-nobands')
    createHeader(htmlTable, words)
    createBody(htmlTable, list, lang, words, frame)

    -- Output
    local styleTag = frame:extensionTag('templatestyles', '', { src = frame:getTitle() .. '/styles.css' })
    return '<div class="artifacts-overview">' .. styleTag .. tostring(htmlTable) .. '</div>'
end

return p