Skylords Reborn
No edit summary
No edit summary
Line 471: Line 471:
 
local args = getArgs(frame)
 
local args = getArgs(frame)
 
local tagArgs = {}
 
local tagArgs = {}
local getInfo = function(aData, affinity, promo)
+
local getInfo = function(aData, affinity, promo, special)
 
local res = ""
 
local res = ""
 
local key = "Z"
 
local key = "Z"
Line 496: Line 496:
 
if text == '' then text = nil end
 
if text == '' then text = nil end
 
 
local icon_name = format("%s%s %s Ability Icon.png", args.name or args[1], promo and " (Promo)" or "", v.name)
+
local icon_name = format("%s%s %s Ability Icon.png", args.name or args[1], promo and " (Promo)" or special or "", v.name)
local icon2_name = format("%s%s %s 2 Ability Icon.png", args.name or args[1], promo and " (Promo)" or "", v.name)
+
local icon2_name = format("%s%s %s 2 Ability Icon.png", args.name or args[1], promo and " (Promo)" or special or "", v.name)
   
 
res =
 
res =
Line 576: Line 576:
 
args[1] ..
 
args[1] ..
 
" (Lost Souls)=" ..
 
" (Lost Souls)=" ..
general_desc(args[1] .. " (Lost Souls)") .. getInfo(get.abilities(args[1] .. " (Lost Souls)"))
+
general_desc(args[1] .. " (Lost Souls)") .. getInfo(get.abilities(args[1] .. " (Lost Souls)"), false, " (Lost Souls)")
 
end
 
end
 
if get.abilities(args[1] .. " (Twilight)") then
 
if get.abilities(args[1] .. " (Twilight)") then
 
tagArgs[#tagArgs + 1] =
 
tagArgs[#tagArgs + 1] =
 
args[1] ..
 
args[1] ..
" (Twilight)=" .. general_desc(args[1] .. " (Twilight)") .. getInfo(get.abilities(args[1] .. " (Twilight)"))
+
" (Twilight)=" .. general_desc(args[1] .. " (Twilight)") .. getInfo(get.abilities(args[1] .. " (Twilight)"), false, " (Twilight)")
 
end
 
end
 
if get.abilities(args[1] .. " (Superpig)") then
 
if get.abilities(args[1] .. " (Superpig)") then
 
tagArgs[#tagArgs + 1] =
 
tagArgs[#tagArgs + 1] =
 
args[1] ..
 
args[1] ..
" (Superpig)=" .. general_desc(args[1] .. " (Superpig)") .. getInfo(get.abilities(args[1] .. " (Superpig)"))
+
" (Superpig)=" .. general_desc(args[1] .. " (Superpig)") .. getInfo(get.abilities(args[1] .. " (Superpig)"), false, " (Superpig)")
 
end
 
end
   

Revision as of 20:03, 16 September 2021

Edit documentation

Description

Handles the creation of the playable cards' appearences in the wiki.

See also


-- Questions? Ask them on Message_Wall:FabianLars or Discord (User:FabianLars for contact details)

local p = {}
local get = require("Module:Card/get")
local has = require("Module:Card/has")
local lib = require("Module:Shared")
local concat = lib.concat
local format = lib.format
local getArgs = lib.getArgs

--- get name, affinity and promo from given cardname
---@param n string name of card in form of 'name', 'name (affinity)' or 'name (Promo)'
---@return string, string|nil, boolean Cardname without parantheses; affinity or nil; true if promo
---@private
local function verifyName(n)
    local aff, promo = nil, false
    local name, var = string.find(n, " (", 1, true)
    name, _ = name and string.sub(n, 1, (name or 0) - 1) or n, nil
    var, _ = string.gsub(string.sub(n, (var or 0) + 1), "%)", "")

    if var ~= name then
        if var == "Promo" then
            promo = true
        elseif var == "Lost Souls" or var == "Twilight" or var == "Superpig" then
            name = format("%s (%s)", name, var)
        elseif var == "Fire" or var == "Frost" or var == "Nature" or var == "Shadow" then
            if has.affinity(name, var) then
                aff = var
            end
        end
    end

    return name, aff, promo
end

local function CardClass(init)
    local self = type(init) == "table" and init or {}
    self.scaling = self.scaling or 0.74
    self.cardupgrade = self.cardupgrade or 0
    self.applied_charges = self.applied_charges or 0
    self.notooltip = self.notooltip or false
    self.infobox = self.infobox or false
    self.orbs = self.orbs or {}

    function self.build()
        local card_container =
            mw.html.create("div"):addClass(format("hidden cv-card-container%s", self.promo and " cv-promo" or "")):cssText(
            format("transform: scale(%s)", self.scaling)
        )
        if self.err then
            card_container:attr("title", self.err or "Error: Check your input")
        end

        local card_artwork =
            mw.html.create("div"):addClass("cv-art"):wikitext(self.artwork_wktxt or ""):cssText(
            self.err and "top: 0;" or ""
        ):done()

        local card_background =
            mw.html.create("div"):addClass(
            "cv-bg c" .. (self.is_upgrade and "u" or "") .. "v-bg-" .. (self.factionLR or "Blank")
        ):wikitext((self.link and self.link ~= "") and ("[[" .. self.link .. "]]") or "")
        if self.spell_background_wktxt then
            card_background:tag("div"):addClass("cv-bg-spell"):wikitext(self.spell_background_wktxt):done()
        end
        card_background:done()

        self.cardupgrade = self.cardupgrade or 0
        self.applied_charges = self.applied_charges or 0

        local upgradeLeft = mw.html.create()

        if self.infobox or self.cardupgrade == 1 then
            upgradeLeft:tag("div"):addClass("cv-upgradeparts cv-U1"):css {
                ["position"] = "absolute",
                ["bottom"] = "168px",
                ["left"] = 0,
                ["z-index"] = 150,
                ["display"] = (self.cardupgrade == 1 and "" or "none")
            }:wikitext(self.upgrade_left_1_wktxt or ""):done()
        end
        if self.infobox or self.cardupgrade == 2 then
            upgradeLeft:tag("div"):addClass("cv-upgradeparts cv-U2"):css {
                ["position"] = "absolute",
                ["bottom"] = "168px",
                ["left"] = 0,
                ["z-index"] = 150,
                ["display"] = (self.cardupgrade == 2 and "" or "none")
            }:wikitext(self.upgrade_left_2_wktxt or ""):done()
        end
        if self.infobox or self.cardupgrade == 3 then
            upgradeLeft:tag("div"):addClass("cv-upgradeparts cv-U3"):css {
                ["position"] = "absolute",
                ["bottom"] = "168px",
                ["left"] = 0,
                ["z-index"] = 150,
                ["display"] = (self.cardupgrade == 3 and "" or "none")
            }:wikitext(self.upgrade_left_3_wktxt or ""):done()
        end
        if self.infobox or self.cardupgrade >= 1 then
            upgradeLeft:tag("div"):addClass("cv-chargeparts cv-C1"):css {
                ["position"] = "absolute",
                ["bottom"] = (self.factionLeft == "Neutral" and "232px" or "233px"),
                ["left"] = (self.factionLeft == "Neutral" and "10px" or "4px"),
                ["z-index"] = 200,
                ["display"] = ((self.applied_charges >= 1 and self.applied_charges <= self.cardupgrade) and "" or "none")
            }:wikitext(self.charge_left_1_wktxt or ""):done()
        end
        if self.infobox or self.cardupgrade >= 2 then
            upgradeLeft:tag("div"):addClass("cv-chargeparts cv-C2"):css {
                ["position"] = "absolute",
                ["bottom"] = (self.factionLeft == "Neutral" and "312px" or "304px"),
                ["left"] = (self.factionLeft == "Neutral" and "10px" or "4px"),
                ["z-index"] = 200,
                ["display"] = ((self.applied_charges >= 2 and self.applied_charges <= self.cardupgrade) and "" or "none")
            }:wikitext(self.charge_left_2_wktxt or ""):done()
        end
        if self.infobox or self.cardupgrade >= 3 then
            upgradeLeft:tag("div"):addClass("cv-chargeparts cv-C3"):css {
                ["position"] = "absolute",
                ["bottom"] = (self.factionLeft == "Neutral" and "394px" or "372px"),
                ["left"] = (self.factionLeft == "Neutral" and "10px" or "4px"),
                ["z-index"] = 200,
                ["display"] = ((self.applied_charges >= 3 and self.applied_charges <= self.cardupgrade) and "" or "none")
            }:wikitext(self.charge_left_3_wktxt or ""):done()
        end

        if self.promo_icon_wktxt then
            upgradeLeft:tag("div"):addClass("cv-promo-icon " .. (self.promo_icon_class or "")):wikitext(
                self.promo_icon_wktxt
            ):done()
        end

        upgradeLeft:done()

        local upgradeRight = mw.html.create()

        if self.infobox or self.cardupgrade >= 1 then
            upgradeRight:tag("div"):addClass("cv-upgradeparts cv-U1"):css {
                ["position"] = "absolute",
                ["bottom"] = "168px",
                ["right"] = 0,
                ["z-index"] = 150,
                ["display"] = (self.cardupgrade >= 1 and "" or "none")
            }:wikitext(self.upgrade_right_1_wktxt or ""):done()
        end
        if self.infobox or self.cardupgrade >= 2 then
            upgradeRight:tag("div"):addClass("cv-upgradeparts cv-U2"):css {
                ["position"] = "absolute",
                ["bottom"] = (self.factionRight == "Neutral" and "248px" or "296px"),
                ["right"] = (self.factionRight == "Neutral" and 0 or "4px"),
                ["z-index"] = 150,
                ["display"] = (self.cardupgrade >= 2 and "" or "none")
            }:wikitext(self.upgrade_right_2_wktxt or ""):done()
        end
        if self.infobox or self.cardupgrade >= 3 then
            upgradeRight:tag("div"):addClass("cv-upgradeparts cv-U3"):css {
                ["position"] = "absolute",
                ["bottom"] = (self.factionRight == "Neutral" and "326px" or "362px"),
                ["right"] = (self.factionRight == "Neutral" and 0 or "4px"),
                ["z-index"] = 150,
                ["display"] = (self.cardupgrade == 3 and "" or "none")
            }:wikitext(self.upgrade_right_3_wktxt or ""):done()
        end
        if self.infobox or self.cardupgrade >= 1 then
            upgradeRight:tag("div"):addClass("cv-chargeparts cv-C1"):css {
                ["position"] = "absolute",
                ["bottom"] = (self.factionRight == "Neutral" and "232px" or "221px"),
                ["right"] = (self.factionRight == "Neutral" and "11px" or "4px"),
                ["z-index"] = 200,
                ["display"] = ((self.applied_charges >= 1 and self.applied_charges <= self.cardupgrade) and "" or "none")
            }:wikitext(self.charge_right_1_wktxt or ""):done()
        end
        if self.infobox or self.cardupgrade >= 2 then
            upgradeRight:tag("div"):addClass("cv-chargeparts cv-C2"):css {
                ["position"] = "absolute",
                ["bottom"] = (self.factionRight == "Neutral" and "312px" or "294px"),
                ["right"] = (self.factionRight == "Neutral" and "11px" or "4px"),
                ["z-index"] = 200,
                ["display"] = ((self.applied_charges >= 2 and self.applied_charges <= self.cardupgrade) and "" or "none")
            }:wikitext(self.charge_right_2_wktxt or ""):done()
        end
        if self.infobox or self.cardupgrade >= 3 then
            upgradeRight:tag("div"):addClass("cv-chargeparts cv-C3"):css {
                ["position"] = "absolute",
                ["bottom"] = (self.factionRight == "Neutral" and "390px" or "362px"),
                ["right"] = (self.factionRight == "Neutral" and "11px" or "4px"),
                ["z-index"] = 200,
                ["display"] = ((self.applied_charges >= 3 and self.applied_charges <= self.cardupgrade) and "" or "none")
            }:wikitext(self.charge_right_3_wktxt or ""):done()
        end
        upgradeRight:done()

        local card_name = mw.html.create("div"):addClass("cv-name"):wikitext(self.display_name or ""):done()

        local card_tokenslot =
            self.is_upgrade and mw.html.create() or
            mw.html.create("div"):addClass(
                format(
                    "cv-slot cv-slot-%s card-viewer-color-%s",
                    self.factionLR == "FireNature" and "FireNature" or (self.factionRight or "blank"),
                    string.lower(self.affinity or "blank")
                )
            ):wikitext(self.affinity and "[[File:Affinity Tokenslot Overlay Blank.png|link=]]" or "")

        local orb1 = mw.html.create("div"):addClass("cv-orb1 cv-orb-" .. (self.orbs[1] or "None")):done()

        local orb2 = mw.html.create("div"):addClass("cv-orb2 cv-orb-" .. (self.orbs[2] or "None")):done()

        local orb3 = mw.html.create("div"):addClass("cv-orb3 cv-orb-" .. (self.orbs[3] or "None")):done()

        local orb4 = mw.html.create("div"):addClass("cv-orb4 cv-orb-" .. (self.orbs[4] or "None")):done()

        local card_power_cost =
            mw.html.create("div"):addClass("cv-cost"):attr("data-cost", self.data_cost_attr or ""):wikitext(
            self.power_cost_wktxt or ""
        ):done()

        local card_charges_squadsize_class =
            mw.html.create("div"):addClass("cv-squad"):tag("span"):addClass("cv-charges"):attr(
            "data-charges",
            self.data_charges_attr or ""
        ):wikitext(self.charges_wktxt or ""):done()
        if self.squadsize and self.squadsize > 1 then
            card_charges_squadsize_class:wikitext(format("x%s", self.squadsize))
        end
        card_charges_squadsize_class:wikitext(format(" %s", self.class or "")):done()

        local card_affinity_icon =
            self.affinity and
            (mw.html.create("div"):addClass("cv-aff-icon cv-aff-icon-" .. self.affinity) or mw.html.create()):done()

        local card_abilities = mw.html.create("div"):addClass("cv-abilities"):node(self.abilities_node or "")

        local card_weapon_type =
            mw.html.create("div"):addClass(
            format("cv-weapon card-viewer-layer-%s", string.lower(self.factionLeft or "blank"))
        ):wikitext(self.weapon_type_wktxt or ""):done()
        local card_counter =
            mw.html.create("div"):addClass(
            format("cv-counter card-viewer-layer-%s", string.lower(self.factionLeft or "blank"))
        ):wikitext(self.counter_wktxt or ""):done()

        local card_damage =
            mw.html.create("div"):addClass("cv-damage"):attr("data-damage", self.data_damage_attr or ""):wikitext(
            self.damage_wktxt or ""
        ):done()

        local card_size_class = mw.html.create("div"):addClass("cv-size card-viewer-unit-size")
        if self.unit_size_wktxt or self.health_wktxt then
            card_size_class:addClass(format("card-viewer-layer-%s", string.lower(self.factionRight or "blank"))):wikitext(
                "[[File:Card_Icon_HP.png||link=]]"
            ):wikitext(self.unit_size_wktxt or "")
        end

        local card_health =
            mw.html.create("div"):addClass("cv-health"):attr("data-health", self.data_health_attr or ""):wikitext(
            self.health_wktxt or ""
        ):done()

        local card_edition_icon =
            mw.html.create("div"):addClass(
            (not self.edition or not self.rarity) and "cv-edition cv-edition-blank" or
                format(
                    "cv-edition cv-edition-%s-%s",
                    string.gsub(self.edition, " ", "-"),
                    string.gsub(self.rarity, " ", "-")
                )
        )

        card_container:node(card_artwork):node(card_background):node(upgradeLeft):node(upgradeRight):node(card_name):node(
            card_tokenslot
        ):node(orb1):node(orb2):node(orb3):node(orb4):node(card_power_cost):node(card_charges_squadsize_class):node(
            card_affinity_icon
        ):node(card_abilities):node(card_weapon_type):node(card_counter):node(card_damage):node(card_size_class):node(
            card_health
        ):node(card_edition_icon):done()

        local return_val =
            mw.html.create("div"):addClass((not self.notooltip and not self.err) and "custom-tooltip" or ""):attr(
            "data-tt-type",
            "card"
        ):attr(
            "data-1",
            (self.full_name or self.display_name) ..
                (self.promo and " (Promo)" or (self.affinity and " (" .. self.affinity .. ")" or ""))
        ):attr("data-2", self.cardupgrade):attr("data-3", self.applied_charges):css {
            ["width"] = format("%spx", 370 * self.scaling),
            ["height"] = format("%spx", 510 * self.scaling),
            ["display"] = "inline-block"
        }:tag("div"):cssText("display:none"):wikitext(self.display_name and "[[" .. self.display_name .. "]]" or ""):done(

        ):node(card_container):allDone()

        if self.filter then
        	local special = {}
        	if self.promo then special[#special+1] = 'Promo' end
        	if self.starter then special[#special+1] = 'Starter' end
        	if self.normal then special[#special+1] = 'Normal' end
        	local nl = (function()
                for _, v in ipairs(self.orbs) do
                    if v and v ~= "Neutral" then
                        return true
                    end
                end
            end)()
            if nl then special[#special+1] = "Non-Legendary" end
        	
            return_val:attr("data-search", self.full_name or self.display_name)
            :attr("data-rarity", self.rarity)
            :attr("data-cost", self.power_cost_wktxt)
            :attr("data-edition", self.editions)
            :attr("data-faction", self.faction)
            :attr("data-counter", self.counter)
            :attr("data-weapontype", self.weapon_type)
            :attr("data-special", concat(special, ','))
            :attr("data-orbs", concat(self.orbs, ","))
            :attr("data-orbsamount", #self.orbs)
            :attr("data-affinities", concat(self.affinity, ","))
            :attr("data-size", self.size)
            :attr("data-type", self.type)
        end

        return return_val
    end

    return self
end

local function ErrorCard(err, scaling)
    return CardClass {
        scaling = scaling,
        display_name = "ERROR",
        artwork_wktxt = "[[File:Quests_warning.png||link=|320px]]",
        abilities_node = mw.html.create("div"):cssText("width: 320px;line-height: 32px;"):wikitext(err),
        err = err
    }.build()
end

local function chargeRules(index)
    local charge_rules = {
        [4] = {1, 1, 1, 1},
        [8] = {4, 2, 1, 1},
        [10] = {5, 2, 2, 1},
        [12] = {6, 2, 2, 2},
        [16] = {8, 3, 3, 2},
        [20] = {10, 4, 3, 3},
        [24] = {12, 4, 4, 4}
    }
    if index then
        return charge_rules[index]
    end
    return charge_rules
end

local function getProgressionArgs(v1, v2, v3, v4)
    if v1 and v1 ~= "" then
        return {texttip = "true", v1, "Base / U-0", v2, "U-1", v3, "U-2", v4, "U-3"}
    elseif v2 and v2 ~= "" then
        return {texttip = "true", v2, "U-1", v3, "U-2", v4, "U-3"}
    elseif v3 and v3 ~= "" then
        return {texttip = "true", v3, "U-2", v4, "U-3"}
    elseif v4 and v4 ~= "" then
        return {texttip = "true", v4, "U-3"}
    else
        return
    end
end

local function getAbilAvail(t)
    if type(t) ~= "table" then
        return nil
    end
    local from, to
    for k, v in ipairs(t) do
        if v and not from then
            from = k - 1
        end
        if from and not v then
            to = k - 2
        end
    end
    if from == 3 then
        return 3
    end
    if from == to then
        if from == 0 then
            return nil
        end
        return from
    else
        return (from or 0) .. "-" .. (to or 3)
    end
end

local function general_desc(key)
    local d = get.description(key)

    if type(d) == "string" and d ~= "" then
        return format('<div class="ability">%s</div>', d)
    end
    if type(d) == "table" and lib.length(d) > 0 then
        return format('<div class="ability">%s</div>', lib.p(getProgressionArgs(d[1], d[2], d[3], d[4])))
    end

    return ""
end

function p.ability(frame)
    local args = getArgs(frame)
    if not (args[1] and args[2]) then return 'Invalid Input' end

    local getAbInfo = function(v, affinity, promo)
    	local text = v.description or ""
        if v.values and v.values[1] then
            local temp = {}
            if type(v.values[1]) == "number" or type(v.values[1]) == "string" then
                temp[#temp + 1] =
                    frame:expandTemplate {
                    title = "p",
                    args = getProgressionArgs(v.values[1], v.values[2], v.values[3], v.values[4])
                }
            else
                for _, vv in ipairs(v.values) do
                    temp[#temp + 1] =
                        frame:expandTemplate {title = "p", args = getProgressionArgs(vv[1], vv[2], vv[3], vv[4])}
                end
            end
            local temp_text, _ = mw.ustring.gsub(text, "%%([^s])", "%%%%%1")
            text = format(temp_text, unpack(temp))
        end

    	local icon_name = format("%s%s %s Ability Icon.png", args[2], promo and " (Promo)" or "", v.name)
    	local icon2_name = format("%s%s %s 2 Ability Icon.png", args[2], promo and " (Promo)" or "", v.name)

    	return frame:expandTemplate {
        title = "Ability",
        args = {
            name = v.name,
            type = v.type,
            cost = type(v.cost) == "table" and
                frame:expandTemplate {
                    title = "p",
                    args = getProgressionArgs(v.cost[1], v.cost[2], v.cost[3], v.cost[4])
                } or
                v.cost,
            affinity_dependency = v.affinity_dependency and affinity or nil,
            upgrade_availability = getAbilAvail(v.upgrade_availability),
            icon = mw.title.new(icon_name, "File").exists and icon_name or nil,
            icon2 = mw.title.new(icon2_name, "File").exists and icon2_name or nil,
            description = frame:preprocess(text),--TODO: remove preprocess once fandom gets their shit together
            card = args[2]
	        }
	    }
    end

    local exts = {'', ' (Fire)', ' (Frost)', ' (Nature)', ' (Shadow)', ' (Promo)', ' (Lost Souls)', ' (Twilight)', ' (Superpig)'}
    local affs = {string.find(args[2], ' (Promo)', 1, true) and get.affinity(args[2]) or nil, 'Fire', 'Frost', 'Nature', 'Shadow', get.affinity(args[2] .. " (Promo)")}
    local abilities = {}

	-- iterate over cards
    for k,v in ipairs(exts) do
    	-- iterate over abilities
    	for _,vv in ipairs(get.abilities(args[2]..v) or {}) do
			if vv.name == args[1] then return getAbInfo(vv, affs[k], k == 6 or string.find(args[2], ' (Promo)', 1, true) ~= nil) end
    	end
    end
end

function p.abilities(frame)
    local args = getArgs(frame)
    local tagArgs = {}
    local getInfo = function(aData, affinity, promo, special)
        local res = ""
        local key = "Z"

        for _, v in ipairs(aData or {}) do
            local text = v.description or ""
            if v.values and v.values[1] then
                local temp = {}
                if type(v.values[1]) == "number" or type(v.values[1]) == "string" then
                    temp[#temp + 1] =
                        frame:expandTemplate {
                        title = "p",
                        args = getProgressionArgs(v.values[1], v.values[2], v.values[3], v.values[4])
                    }
                else
                    for _, vv in ipairs(v.values) do
                        temp[#temp + 1] =
                            frame:expandTemplate {title = "p", args = getProgressionArgs(vv[1], vv[2], vv[3], vv[4])}
                    end
                end
                local temp_text, _ = mw.ustring.gsub(text, "%%([^s])", "%%%%%1")
                text = format(temp_text, unpack(temp))
            end
            if text == '' then text = nil end
            
            local icon_name = format("%s%s %s Ability Icon.png", args.name or args[1], promo and " (Promo)" or special or "", v.name)
            local icon2_name = format("%s%s %s 2 Ability Icon.png", args.name or args[1], promo and " (Promo)" or special or "", v.name)

            res =
                res ..
                frame:expandTemplate {
                    title = "Ability",
                    args = {
                        hotkey = (v.type == "Active" or v.type == "Toggle") and key or nil,
                        name = v.name,
                        type = v.type,
                        cost = type(v.cost) == "table" and
                            frame:expandTemplate {
                                title = "p",
                                args = getProgressionArgs(v.cost[1], v.cost[2], v.cost[3], v.cost[4])
                            } or
                            v.cost,
                        affinity_dependency = v.affinity_dependency and affinity or nil,
                        upgrade_availability = getAbilAvail(v.upgrade_availability),
                        icon = mw.title.new(icon_name, "File").exists and icon_name or nil,
                        icon2 = mw.title.new(icon2_name, "File").exists and icon2_name or nil,
                        details = args["ability_details_" .. (tostring(v.name or ""):lower():gsub("%s+", "_"))] and
                            frame:preprocess(
                                args["ability_details_" .. (tostring(v.name or ""):lower():gsub("%s+", "_"))]
                            ) or
                            nil,
                        description = frame:preprocess(text),--TODO: remove preprocess once fandom gets their shit together
                    }
                }

            if v.type == "Active" or v.type == "Toggle" then
                if key == "Z" then
                    key = "X"
                elseif key == "X" then
                    key = "C"
                elseif key == "C" then
                    key = "V"
                end
            end
        end
        return res
    end

    local promo_aff = get.affinity(args[1] .. " (Promo)")

    if get.abilities(args[1]) then
        tagArgs[#tagArgs + 1] = args[1] .. "=" .. general_desc(args[1]) .. getInfo(get.abilities(args[1]))
    end
    if get.abilities(args[1] .. " (Fire)") then
        tagArgs[#tagArgs + 1] =
            args[1] ..
            " (Fire)=" .. general_desc(args[1] .. " (Fire)") .. getInfo(get.abilities(args[1] .. " (Fire)"), "Fire")
    end
    if get.abilities(args[1] .. " (Frost)") then
        tagArgs[#tagArgs + 1] =
            args[1] ..
            " (Frost)=" .. general_desc(args[1] .. " (Frost)") .. getInfo(get.abilities(args[1] .. " (Frost)"), "Frost")
    end
    if get.abilities(args[1] .. " (Nature)") then
        tagArgs[#tagArgs + 1] =
            args[1] ..
            " (Nature)=" ..
                general_desc(args[1] .. " (Nature)") .. getInfo(get.abilities(args[1] .. " (Nature)"), "Nature")
    end
    if get.abilities(args[1] .. " (Shadow)") then
        tagArgs[#tagArgs + 1] =
            args[1] ..
            " (Shadow)=" ..
                general_desc(args[1] .. " (Shadow)") .. getInfo(get.abilities(args[1] .. " (Shadow)"), "Shadow")
    end
    if get.abilities(args[1] .. " (Promo)") then
        tagArgs[#tagArgs + 1] =
            args[1] ..
            " (Promo)=" ..
                general_desc(args[1] .. " (Promo)") .. getInfo(get.abilities(args[1] .. " (Promo)"), promo_aff, true)
    end
    if get.abilities(args[1] .. " (Lost Souls)") then
        tagArgs[#tagArgs + 1] =
            args[1] ..
            " (Lost Souls)=" ..
                general_desc(args[1] .. " (Lost Souls)") .. getInfo(get.abilities(args[1] .. " (Lost Souls)"), false, " (Lost Souls)")
    end
    if get.abilities(args[1] .. " (Twilight)") then
        tagArgs[#tagArgs + 1] =
            args[1] ..
            " (Twilight)=" .. general_desc(args[1] .. " (Twilight)") .. getInfo(get.abilities(args[1] .. " (Twilight)"), false, " (Twilight)")
    end
    if get.abilities(args[1] .. " (Superpig)") then
        tagArgs[#tagArgs + 1] =
            args[1] ..
            " (Superpig)=" .. general_desc(args[1] .. " (Superpig)") .. getInfo(get.abilities(args[1] .. " (Superpig)"), false, " (Superpig)")
    end

    return (#tagArgs or 0) <= 1 and (tagArgs[1] or ""):gsub("^.-=", "", 1) or
        frame:preprocess("<tabber>" .. concat(tagArgs, "|-|") .. "</tabber>")
end

--- returns html for a card to display
---@param frame table mw.frame object
---@return string card as mw.html
---@public
function p.card(frame)
    local args = getArgs(frame)

    if not get.exists(args.name or args[1]) then
        return ErrorCard(
            format('Card or Variant "%s" not found', args.name or args[1]),
            args.scaling or args.displayscaling or 0.74
        )
    end

    local data = get.card(args.name or args[1])

    local card = CardClass()
    local charge_rules = chargeRules()
    local name = args.name or args[1] or "Name missing"
    local display_name, aff, promo = verifyName(args.name or args[1] or "Name missing")
    if data.affinity then
        aff = data.affinity
    end
    if data.promo then
        promo = data.promo
    end
    if data.temporary_card then
        promo = true
    end
    local scaling = args.scaling or data.scaling or args.displayscaling or data.displayscaling or 0.74
    --local err = nil
    local check_size = {
        S = true,
        M = true,
        L = true,
        XL = true
    }

    local link = display_name
    if data.nolink or args.nolink then
        link = ""
    end

    local res = data.orbs and get["factions"](name, data.orbs)
    local factionLR = data.orbs and (res[1] ~= res[2] and res[1] .. res[2] or res[1]) or "Blank"
    local factionLeft = res and res[1] or nil
    local factionRight = res and res[2] or nil

    card.filter = args.filter
    card.infobox = args.infobox
    card.link = link
    card.type = data.type
    card.faction = get.faction(name)
    card.factionLR = factionLR
    card.factionLeft = factionLeft
    card.factionRight = factionRight
    card.affinity = aff
    card.notooltip = args.notooltip or data.notooltip or false
    card.scaling = scaling
    card.artwork_wktxt =
        format(
        data.artwork and format("[[File:%s||link=|320px]]", data.artwork) or "[[File:%s%s_Card_Artwork.png||link=]]",
        display_name,
        promo and "_(Promo)" or ""
    )
    card.spell_background_wktxt =
        data.type == "Spell" and format("[[File:Spell_Card_Overlay_%s.png||link=]]", factionLR) or nil
    local cardupgrade = promo and 3 or (tonumber(data.cardupgrade) or tonumber(args.cardupgrade) or 0)
    local applied_charges = promo and 3 or (tonumber(data.applied_charges) or tonumber(args.applied_charges) or 0)
    card.cardupgrade = cardupgrade
    card.applied_charges = applied_charges
    if promo and not data.temporary_card then
        card.promo = true
        card.promo_icon_wktxt = format("[[File:Promo_Icon_%s.png||link=]]", factionLeft)
    elseif data.starter_card and (args.infobox or (cardupgrade or 0) == 0) then
        card.promo_icon_class = "cv-upgradeparts cv-A0"
        card.promo_icon_wktxt = format("[[File:Starter_Icon_%s.png||link=]]", factionLeft)
    elseif data.temporary_card then
        card.promo_icon_wktxt = format("[[File:Temporary_Icon_%s.png||link=]]", factionLeft)
    end
    card.upgrade_left_1_wktxt = format("[[File:%s_Upgrade_1_Left.png||link=]]", factionLeft)
    card.upgrade_left_2_wktxt = format("[[File:%s_Upgrade_2_Left.png||link=]]", factionLeft)
    card.upgrade_left_3_wktxt = format("[[File:%s_Upgrade_3_Left.png||link=]]", factionLeft)
    card.charge_left_1_wktxt =
        factionLeft == "Neutral" and "[[File:Neutral_Charge_All.png||link=]]" or
        format("[[File:%s_Charge_1_Left.png||link=]]", factionLeft)
    card.charge_left_2_wktxt =
        factionLeft == "Neutral" and "[[File:Neutral_Charge_All.png||link=]]" or
        format("[[File:%s_Charge_2_Left.png||link=]]", factionLeft)
    card.charge_left_3_wktxt =
        factionLeft == "Neutral" and "[[File:Neutral_Charge_All.png||link=]]" or
        format("[[File:%s_Charge_3_Left.png||link=]]", factionLeft)
    card.upgrade_right_1_wktxt = format("[[File:%s_Upgrade_1_Right.png||link=]]", factionRight)
    card.upgrade_right_2_wktxt =
        format("[[File:%s_Upgrade_%s_Right.png||link=]]", factionRight, factionRight == "Neutral" and "1" or "2")
    card.upgrade_right_3_wktxt =
        format("[[File:%s_Upgrade_%s_Right.png||link=]]", factionRight, factionRight == "Neutral" and "1" or "3")
    card.charge_right_1_wktxt =
        factionRight == "Neutral" and "[[File:Neutral_Charge_All.png||link=]]" or
        format("[[File:%s_Charge_1_Right.png||link=]]", factionRight)
    card.charge_right_2_wktxt =
        factionRight == "Neutral" and "[[File:Neutral_Charge_All.png||link=]]" or
        format("[[File:%s_Charge_2_Right.png||link=]]", factionRight)
    card.charge_right_3_wktxt =
        factionRight == "Neutral" and "[[File:Neutral_Charge_All.png||link=]]" or
        format("[[File:%s_Charge_3_Right.png||link=]]", factionRight)
    card.full_name = display_name
    card.display_name = display_name:gsub(" %(Lost Souls%)", ""):gsub(" %(Twilight%)", ""):gsub(" %(Superpig%)", "")
    local dataorbs = {}
    for k, v in ipairs(data.orbs or {}) do
        dataorbs[k] = v
    end
    card.orbs = dataorbs
    card.data_cost_attr = format('["%s"]', concat(data.power_cost, '","'))
    card.power_cost_wktxt = type(data.power_cost) == "table" and data.power_cost[cardupgrade + 1] or data.power_cost
    local actualChargeRule =
        (promo and {data.charges or 4, 0, 0, 0} or
        (type(data.charges) == table and data.charges or (charge_rules[data.charges or 4]) or charge_rules[4]))
    local actualCurrentCharge
    for i = 0, applied_charges do
        actualCurrentCharge = (actualCurrentCharge or 0) + actualChargeRule[i + 1] or 0
    end
    card.charges_wktxt = actualCurrentCharge
    card.data_charges_attr = format('["%s"]', concat(actualChargeRule or {}, '","'))
    card.squadsize = data.squadsize
    card.class = data.class
    card.weapon_type = data.weapon_type
    card.weapon_type_wktxt =
        (data.weapon_type and data.type ~= "Spell") and
        format("[[File:Card_Icon_%s_Unit.png||link=]]", data.weapon_type) or
        ""

    card.counter = data.counter
    card.counter_wktxt =
        (data.weapon_type and data.weapon_type ~= "Special" and data.type ~= "Spell" and
        check_size[data.counter or "Nope"]) and
        format("[[File:Card_Icon_Unit_Size_%s.png|38px|link=]]", data.counter) or
        ""

    card.data_damage_attr = format('["%s"]', concat(data.damage, '","'))
    card.damage_wktxt = type(data.damage) == "table" and data.damage[cardupgrade + 1] or data.damage
    card.unit_size = check_size[data.size or "Nope"] and data.size or nil
    card.unit_size_wktxt =
        check_size[data.size or "Nope"] and format("[[File:Card_Icon_Unit_Size_%s.png|38px|link=]]", data.size) or nil
    card.data_health_attr = format('["%s"]', concat(data.health, '","'))
    card.health_wktxt = type(data.health) == "table" and data.health[cardupgrade + 1] or data.health
    card.edition = data.edition
    card.rarity = data.rarity

    card.abilities_node = data.abilities_done
    if not card.abilities_node then
        local abilities_node = mw.html.create()
        local ab_count = 0
        for k, v in ipairs(data.abilities or {}) do
            if ab_count < 4 and not v.hide_on_card then
                ab_count = ab_count + 1
                local abil = mw.html.create("div")
                local avail = v.upgrade_availability
                if avail then
                    local s = ""
                    local from
                    for k, v in ipairs(avail) do
                        if v and not from then
                            from = k - 1
                        end
                        if v then
                            s = s .. " cv-A" .. (k - 1)
                        end
                    end
                    abil:addClass("cv-upgradeparts" .. s)
                    if from > cardupgrade then
                        abil:cssText("display: none;")
                    end
                end
                if v["type"] then
                    abil:addClass(
                        format(
                            "cv-abil cv-abil-%s%s",
                            string.lower(v["type"]),
                            v["affinity_dependency"] and "-" .. string.lower(aff) or ""
                        )
                    )
                else
                    abil:addClass("cv-abil-none")
                end
                abil:wikitext(v["name"])
                if not promo then
                    local a = 0
                    for i = 1, 3 do
                        local avail = type(v.upgrade_availability) ~= "table" or v.upgrade_availability[i] == true
                        if
                            avail and
                                ((v.upgrade_text and tostring(v.upgrade_text[i] or ""):len() > 0) or
                                    (type(v.cost) == "table" and v.cost[i] ~= v.cost[i + 1] or false))
                         then
                            a = a + 1
                        end
                        if a ~= 0 then
                            abil:tag("span"):addClass(format("cv-upgradeparts cv-abil-upgrade-%s cv-A%s", a, i)):css(
                                "display",
                                (i == cardupgrade and "" or "none")
                            )
                        end
                    end
                end
                abil:allDone()
                abilities_node:node(abil)
            end
        end
        card.abilities_node = abilities_node
    end

    return card.build()
end

function p.count(frame)
    local args = getArgs(frame)

    local i = 0
    local list = get.list()

    if args[1] ~= "" and args[2] ~= "" and args[1] ~= "cardtype" then
        for _, k in ipairs(list) do
            if get[args[1]](k) == args[2] then
                i = i + 1
            end
        end
    else
        if (args[1] == "cardtype" and args[2] == "cards") or args[1] == nil or args[1] == "" then
            for _, k in ipairs(list) do
                if not k:find "(Promo)" then
                    i = i + 1
                end
            end
        elseif (args[1] == "cardtype" and args[2] == "promos") or args[1] == nil or args[1] == "" then
            for _, k in ipairs(list) do
                if k:find "(Promo)" then
                    i = i + 1
                end
            end
        end
    end

    return i
end

function p.custom_card(frame)
    local args = getArgs(frame)

    local name = args.name or args[1] or ""
    local link = args.link or ""

    if args.orbs then
        args.orbs = mw.text.split(args.orbs, "%s*,%s*")
        for k, v in ipairs(args.orbs or {}) do
            args.orbs[k] = v:gsub("^%l", string.upper)
        end
    end

    local res = args.orbs and get.factions(name, args.orbs) or {}
    local factionLR = res[1] and (res[1] ~= res[2] and res[1] .. res[2] or res[1]) or "Blank"
    local factionLeft = res[1]
    local factionRight = res[2]

    if args.type then
        args.type = args.type:gsub("^%l", string.upper)
    end

    if args.edition then
        args.edition = args.edition:gsub("^%l", string.upper)
        if args.edition == "Lost souls" then
            args.edition = "Lost Souls"
        end
    end

    if args.rarity then
        args.rarity = args.rarity:gsub("^%l", string.upper)
        if args.rarity == "Ultra rare" then
            args.rarity = "Ultra Rare"
        end
    end

    if args.weapon_type then
        args.weapon_type = args.weapon_type:gsub("^%l", string.upper)
    end

    if string.lower(args.affinity or "none") == "none" then
        args.affinity = nil
    end
    if args.affinity then
        args.affinity = args.affinity:gsub("^%l", string.upper)
    end

    if args.counter then
        args.counter = string.upper(args.counter)
    end

    if args.size then
        args.size = string.upper(args.size)
    end

    local promo_txt, promo_class
    if type(args.upgrade) == "string" then
        if args.upgrade:lower() == "promo" then
            args.promo = true
            args.cardupgrade = 3
            args.applied_charges = 3
            promo_txt = format("[[File:Promo_Icon_%s.png||link=]]", factionLeft)
        end
        if args.upgrade:lower() == "starter" then
            args.starter_card = true
            promo_class = "cv-upgradeparts cv-A0"
            promo_txt = format("[[File:Starter_Icon_%s.png||link=]]", factionLeft)
        end
        if args.upgrade:lower() == "temp" or args.upgrade:lower() == "temporary" or args.upgrade:lower() == "tome" then
            args.temporary_card = true
            args.cardupgrade = 3
            args.applied_charges = 3
            promo_txt = format("[[File:Temporary_Icon_%s.png||link=]]", factionLeft)
        end
    else
        args.cardupgrade = args.upgrade
    end

    local abilities_done = mw.html.create()
    for i = 1, 5 do
        local abil = mw.html.create("div")
        if args["ability_" .. i .. "_type"] and string.lower(args["ability_" .. i .. "_type"]) ~= "none" then
            abil:addClass(
                format(
                    "cv-abil cv-abil-%s%s",
                    string.lower(args["ability_" .. i .. "_type"]),
                    (args["ability_" .. i .. "_affinity"] == true or
                        string.lower(args["ability_" .. i .. "_affinity"] or "") == "true") and
                        ("-" .. string.lower(args.affinity)) or
                        ""
                )
            )
        end
        abil:wikitext(args["ability_" .. i .. "_name"] or "")
        if
            tonumber(args["ability_" .. i .. "_upgrade"]) and tonumber(args["ability_" .. i .. "_upgrade"]) > 0 and
                tonumber(args["ability_" .. i .. "_upgrade"]) <= 3
         then
            abil:wikitext(
                format("[[File:Card_Icon_Upgrade_Status_0%s.png||link=]]", args["ability_" .. i .. "_upgrade"])
            )
        end

        abil:allDone()
        abilities_done:node(abil)
    end
    abilities_done:allDone()

    local check_size = {
        S = true,
        M = true,
        L = true,
        XL = true
    }

    return CardClass {
        scaling = args.displayscaling or args.scaling,
        affinity = args.affinity,
        factionLR = factionLR,
        factionLeft = factionLeft,
        factionRight = factionRight,
        display_name = name,
        artwork_wktxt = format("[[File:%s||link=|320px]]", args.artwork),
        spell_background_wktxt = args.type == "Spell" and format("[[File:Spell_Card_Overlay_%s.png||link=]]", factionLR) or
            nil,
        cardupgrade = args.cardupgrade,
        applied_charges = args.applied_charges,
        upgrade_left_1_wktxt = format("[[File:%s_Upgrade_1_Left.png||link=]]", factionLeft),
        upgrade_left_2_wktxt = format("[[File:%s_Upgrade_2_Left.png||link=]]", factionLeft),
        upgrade_left_3_wktxt = format("[[File:%s_Upgrade_3_Left.png||link=]]", factionLeft),
        charge_left_1_wktxt = factionLeft == "Neutral" and "[[File:Neutral_Charge_All.png||link=]]" or
            format("[[File:%s_Charge_1_Left.png||link=]]", factionLeft),
        charge_left_2_wktxt = factionLeft == "Neutral" and "[[File:Neutral_Charge_All.png||link=]]" or
            format("[[File:%s_Charge_2_Left.png||link=]]", factionLeft),
        charge_left_3_wktxt = factionLeft == "Neutral" and "[[File:Neutral_Charge_All.png||link=]]" or
            format("[[File:%s_Charge_3_Left.png||link=]]", factionLeft),
        upgrade_right_1_wktxt = format("[[File:%s_Upgrade_1_Right.png||link=]]", factionRight),
        upgrade_right_2_wktxt = format(
            "[[File:%s_Upgrade_%s_Right.png||link=]]",
            factionRight,
            factionRight == "Neutral" and "1" or "2"
        ),
        upgrade_right_3_wktxt = format(
            "[[File:%s_Upgrade_%s_Right.png||link=]]",
            factionRight,
            factionRight == "Neutral" and "1" or "3"
        ),
        charge_right_1_wktxt = factionRight == "Neutral" and "[[File:Neutral_Charge_All.png||link=]]" or
            format("[[File:%s_Charge_1_Right.png||link=]]", factionRight),
        charge_right_2_wktxt = factionRight == "Neutral" and "[[File:Neutral_Charge_All.png||link=]]" or
            format("[[File:%s_Charge_2_Right.png||link=]]", factionRight),
        charge_right_3_wktxt = factionRight == "Neutral" and "[[File:Neutral_Charge_All.png||link=]]" or
            format("[[File:%s_Charge_3_Right.png||link=]]", factionRight),
        promo_icon_wktxt = promo_txt,
        promo_icon_class = promo_class,
        orbs = args.orbs or {},
        power_cost_wktxt = args.power_cost,
        charges_wktxt = args.charges,
        unit_size_wktxt = check_size[args.size or "Nope"] and
            format("[[File:Card_Icon_Unit_Size_%s.png||link=]]", args.size) or
            nil,
        squadsize = args.squadsize,
        class = args.class,
        damage_wktxt = args.damage,
        weapon_type_wktxt = (args.weapon_type and args.type ~= "Spell") and
            format(
                "[[File:Card_Icon_%s%s.png||link=]]",
                args.weapon_type,
                (check_size[args.counter or "Nope"] and args.weapon_type ~= "Special") and
                    format("_Unit_Countering_Size_%s", args.counter) or
                    (args.weapon_type ~= "Special" and "_Unit" or ""),
                link
            ) or
            ((not args.weapon_type and check_size[args.counter or "Nope"] and args.type ~= "Spell") and
                format(
                    '<span style="padding-left: 14px;">[[File:Card_Icon_Unit_Size_%s.png||link=]]</span>',
                    args.counter
                )),
        health_wktxt = args.health,
        edition = args.edition,
        rarity = args.rarity,
        notooltip = true,
        abilities_node = abilities_done
    }.build()
end

--- returns a list of cards formatted as a deck display
---@param frame table mw.frame object
---@return string deck as mw.html
---@public
function p.deck(frame)
    local args = getArgs(frame)

    local res = mw.html.create("div"):cssText("display:flex;flex-wrap:wrap;")
    if args.maxperrow then
        res:cssText(
            format(
                "max-width:100%%;width:calc(%spx * %s);",
                args.icononly and 82 * (tonumber(args.scaling) or tonumber(args.displayscaling) or 1) or
                    370 * (tonumber(args.scaling) or tonumber(args.displayscaling) or 0.22),
                args.maxperrow
            )
        )
    end
    for i = 1, 20 do
        local t = mw.text.split(args[i] or "", ";", true)
        if t[1] ~= "" then
            if args.icononly then
                res:node(
                    p.icon {
                        t[1],
                        size = format("%spx", (args.scaling or args.displayscaling or 1) * 80),
                        icononly = true,
                        nolink = args.nolink == true and true or false,
                        c = tonumber(t[2] or 0),
                        u = tonumber(t[3] or 0)
                    }
                )
            else
                res:node(
                    p.card {
                        t[1],
                        cardupgrade = tonumber(t[2] or 0),
                        applied_charges = tonumber(t[3] or 0),
                        scaling = args.scaling or args.displayscaling or "0.22",
                        nolink = args.nolink == true and true or false
                    }
                )
            end
        end
    end

    return res
end

function p.for_each(frame)
    local args = getArgs(frame)
    local list = get.list()
    local result

    if args[1] == "list_of_card_ids" then
        result = mw.html.create()

        for k, v in ipairs(list) do
            local id = get.card_id(v) or 0
            local ci = p.icon {v}
            local id0 = tostring(id)
            local id1 = string.format("1%06d", id)
            local id2 = string.format("2%06d", id)
            local id3 = string.format("3%06d", id)

            result:tag("tr"):addClass("search-item"):attr("data-search", v .. id0):tag("td"):node(ci):done():tag("td"):wikitext(
                id0
            ):done():tag("td"):wikitext(id1):done():tag("td"):wikitext(id2):done():tag("td"):wikitext(id3):done():done()
        end
    elseif args[1] == "list_of_card_upgrade_drop_locations" then
        result = mw.html.create()
        local mapData = mw.loadData("Module:Map/data")

        for k, v in ipairs(list) do
            local ci = p.icon {v}
            local drops = get.upgrade_locations(v)
            local ed = get.edition(v)
            local ra = get.rarity(v)
            local raso = 0
            if ra == "Common" then
                raso = 1
            elseif ra == "Uncommon" then
                raso = 2
            elseif ra == "Rare" then
                raso = 3
            elseif ra == "Ultra Rare" then
                raso = 4
            end
            if drops then
                local d1 = drops[1] or ""
                local d2 = drops[2] or ""
                local d3 = drops[3] or ""
                local dd1 = drops[1] and format("[[File:%s_Minimap.jpg|20px|link=%s]] [[%s]]", d1, d1, d1) or ""
                local dd2 = drops[2] and format("[[File:%s_Minimap.jpg|20px|link=%s]] [[%s]]", d2, d2, d2) or ""
                local dd3 = drops[3] and format("[[File:%s_Minimap.jpg|20px|link=%s]] [[%s]]", d3, d3, d3) or ""

                if drops[1] and not mapData[d1].difficulties["standard"] then
                    dd1 =
                        '<span style="color:red;">Data Error: Upgrade drop location does not match available map difficulty.</span>'
                end
                if drops[2] and not mapData[d2].difficulties["advanced"] then
                    dd2 =
                        '<span style="color:red;">Data Error: Upgrade drop location does not match available map difficulty.</span>'
                end
                if drops[3] and not mapData[d3].difficulties["expert"] then
                    dd3 =
                        '<span style="color:red;">Data Error: Upgrade drop location does not match available map difficulty.</span>'
                end

                local search = v .. d1
                if d2 ~= d1 then
                    search = search .. d2
                end
                if d3 ~= d2 and d3 ~= d1 then
                    search = search .. d3
                end

                result:tag("tr"):addClass("search-item"):attr("data-search", search):tag("td"):node(ci):done():tag("td"):attr(
                    "data-sort-value",
                    raso
                ):attr("title", ra):wikitext(format("[[File:Edition Icon %s %s.png|x22px|link=]]", ed, ra)):done():tag(
                    "td"
                ):wikitext(dd1):done():tag("td"):wikitext(dd2):done():tag("td"):wikitext(dd3):done():done()
            end
        end
    elseif args[1] == "list_of_booster_pack_contents" then
        result = mw.html.create()
        local counter = {
            [1] = 0,
            [2] = 0,
            [3] = 0,
            [4] = 0,
            [5] = 0,
            [6] = 0,
            [7] = 0,
            [8] = 0,
            [9] = 0,
            [10] = 0,
            [11] = 0
        }

        for k, v in ipairs(list) do
            local name = v:gsub(" %(Fire%)", ""):gsub(" %(Frost%)", ""):gsub(" %(Nature%)", ""):gsub(" %(Shadow%)", "")
            local ci = p.icon {[1] = v, ["hide_affinity"] = true}
            local packs = get.boosters(v)

            local p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10 = "X", "X", "X", "X", "X", "X", "X", "X", "X", "X", "X"
            local c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10 =
                "nay",
                "nay",
                "nay",
                "nay",
                "nay",
                "nay",
                "nay",
                "nay",
                "nay",
                "nay",
                "nay"
            local search = name

            if packs[1] then
                p0 = "✔"
                c0 = "yea"
                counter[1] = counter[1] + 1
            else
                search = search .. "None"
            end

            for _, pack in ipairs(packs) do
                search = search .. pack
                if pack == "Fire" then
                    p1 = "✔"
                    c1 = "yea"
                    counter[2] = counter[2] + 1
                elseif pack == "Frost" then
                    p2 = "✔"
                    c2 = "yea"
                    counter[3] = counter[3] + 1
                elseif pack == "Nature" then
                    p3 = "✔"
                    c3 = "yea"
                    counter[4] = counter[4] + 1
                elseif pack == "Shadow" then
                    p4 = "✔"
                    c4 = "yea"
                    counter[5] = counter[5] + 1
                elseif pack == "Amii" then
                    p5 = "✔"
                    c5 = "yea"
                    counter[6] = counter[6] + 1
                elseif pack == "Bandit" then
                    p6 = "✔"
                    c6 = "yea"
                    counter[7] = counter[7] + 1
                elseif pack == "Fire/Frost" then
                    p7 = "✔"
                    c7 = "yea"
                    counter[8] = counter[8] + 1
                elseif pack == "Lost Souls" then
                    p8 = "✔"
                    c8 = "yea"
                    counter[9] = counter[9] + 1
                elseif pack == "Stonekin" then
                    p9 = "✔"
                    c9 = "yea"
                    counter[10] = counter[10] + 1
                elseif pack == "Twilight" then
                    p10 = "✔"
                    c10 = "yea"
                    counter[11] = counter[11] + 1
                end
            end

            result:tag("tr"):addClass("search-item"):attr("data-search", search):tag("td"):node(ci):done():tag("td"):addClass(
                c0
            ):wikitext(p0):done():tag("td"):addClass(c1):wikitext(p1):done():tag("td"):addClass(c2):wikitext(p2):done():tag(
                "td"
            ):addClass(c3):wikitext(p3):done():tag("td"):addClass(c4):wikitext(p4):done():tag("td"):addClass(c5):wikitext(
                p5
            ):done():tag("td"):addClass(c6):wikitext(p6):done():tag("td"):addClass(c7):wikitext(p7):done():tag("td"):addClass(
                c8
            ):wikitext(p8):done():tag("td"):addClass(c9):wikitext(p9):done():tag("td"):addClass(c10):wikitext(p10):done(

            ):done()
        end
        result =
            mw.html.create():tag("tr"):addClass("tr-force-show"):tag("th"):cssText(
            "width: 30%; text-align:left; background-color: #092949"
        ):wikitext("Card Name"):done():tag("th"):cssText(
            "width: 6%; background-color: #092949; padding: 6px 0; text-align: center"
        ):wikitext("[[File:General Booster Pack HD.png|40px|link=]]"):attr(
            "title",
            "General Booster Pack (Card Pool: " .. counter[1] .. ")"
        ):done():tag("th"):cssText("width: 6%; background-color: #092949; padding: 6px 0; text-align: center"):wikitext(
            "[[File:Fire Booster Pack HD.png|40px|link=]]"
        ):attr("title", "Fire Booster Pack (Card Pool: " .. counter[2] .. ")"):done():tag("th"):cssText(
            "width: 6%; background-color: #092949; padding: 6px 0; text-align: center"
        ):wikitext("[[File:Frost Booster Pack HD.png|40px|link=]]"):attr(
            "title",
            "Frost Booster Pack (Card Pool: " .. counter[3] .. ")"
        ):done():tag("th"):cssText("width: 6%; background-color: #092949; padding: 6px 0; text-align: center"):wikitext(
            "[[File:Nature Booster Pack HD.png|40px|link=]]"
        ):attr("title", "Nature Booster Pack (Card Pool: " .. counter[4] .. ")"):done():tag("th"):cssText(
            "width: 6%; background-color: #092949; padding: 6px 0; text-align: center"
        ):wikitext("[[File:Shadow Booster Pack HD.png|40px|link=]]"):attr(
            "title",
            "Shadow Booster Pack (Card Pool: " .. counter[5] .. ")"
        ):done():tag("th"):cssText("width: 6%; background-color: #092949; padding: 6px 0; text-align: center"):wikitext(
            "[[File:Amii Booster Pack HD.png|40px|link=]]"
        ):attr("title", "Amii Booster Pack (Card Pool: " .. counter[6] .. ")"):done():tag("th"):cssText(
            "width: 6%; background-color: #092949; padding: 6px 0; text-align: center"
        ):wikitext("[[File:Bandit Booster Pack HD.png|40px|link=]]"):attr(
            "title",
            "Bandit Booster Pack (Card Pool: " .. counter[7] .. ")"
        ):done():tag("th"):cssText("width: 6%; background-color: #092949; padding: 6px 0; text-align: center"):wikitext(
            "[[File:Fire Frost Booster Pack HD.png|40px|link=]]"
        ):attr("title", "Fire Frost Booster Pack (Card Pool: " .. counter[8] .. ")"):done():tag("th"):cssText(
            "width: 6%; background-color: #092949; padding: 6px 0; text-align: center"
        ):wikitext("[[File:Lost Souls Booster Pack HD.png|40px|link=]]"):attr(
            "title",
            "Lost Souls Booster Pack (Card Pool: " .. counter[9] .. ")"
        ):done():tag("th"):cssText("width: 6%; background-color: #092949; padding: 6px 0; text-align: center"):wikitext(
            "[[File:Stonekin Booster Pack HD.png|40px|link=]]"
        ):attr("title", "Stonekin Booster Pack (Card Pool: " .. counter[10] .. ")"):done():tag("th"):cssText(
            "width: 6%; background-color: #092949; padding: 6px 0; text-align: center"
        ):wikitext("[[File:Twilight Booster Pack HD.png|40px|link=]]"):attr(
            "title",
            "Twilight Booster Pack (Card Pool: " .. counter[11] .. ")"
        ):done():done():node(result:done()):done()
    end

    return result
end

--- returns values from Card/data for a card
---@param frame table mw.frame object
---@return string value from card
---@public
function p.get(frame)
    local args = getArgs(frame)

    local cardname = (args[2]:find("affini")) and args[1] or get.verified_name(args[1])
    local val = args[2]
    local returntype = args[3]
    -- 'return' one of: array, count, 'index' (eg 1).
    -- nil returns default (tables as array (wikitext-string separated by ';'), single value as single value)

    if not get[val] then
        return format("No getter for %s", val)
    end

    -- if not get['card'](cardname) then return format('Card %s not found (fn 'get')', cardname) end
    --if not get[val](cardname, cardtype) then return format('Value %s not found for %s (%s)', val, cardname, get[val](cardname, cardtype)) end

    local returnval = cardname == "list" and get.list() or get[val](cardname)

    if returnval == nil then
        return format('Card %s or value %s not found (fn "get")', cardname, val)
    end

    if returntype == "count" then
        local i = 0
        for _ in pairs(returnval or {}) do
            i = i + 1
        end
        return i
    elseif returntype == "array" then
        if not type(returnval) == "table" then
            return format("Value %s not an array (lua table)", val)
        end
        return concat(returnval, "; ")
    elseif type(returntype) == "number" and returntype == math.floor(returntype) and returntype < 10 then
        return returnval[tonumber(returntype)]
    else
        -- Something's not workin' here
        if type(returnval) == "table" then
            return concat(returnval, "; ")
        elseif type(returnval) == "string" or type(returnval) == "number" or type(returnval) == "boolean" then
            return returnval
        else
            return "Something went wrong. Check: card, value, return and Module:Card/data"
        end
    end
end

--- check if card has value
---@param frame table mw.frame object
---@return boolean true if card/data contains value for card; else nothing
---@public
function p.has(frame)
    local args = getArgs(frame)

    if has[args[2]] and has[args[2]](args[1]) then
        return true
    end
    local cardname = args[2]:find("affini") and args[1] or get.verified_name(args[1])
    if get[args[2]] and get[args[2]](cardname) then
        return true
    end
end

--- returns the content for Template:Card_icon
---@param frame table mw.frame object
---@return string icon_text or icon as mw.html
---@public
function p.icon(frame)
    local args = getArgs(frame)

    local name, var

    if string.find(args[1], " (", 1, true) and string.find(args[1], ")", 1, true) then
        name, var = string.match(args[1], "(.+) %((.+)%)")
    else
        name = args[1]
    end

    local ran, val_or_err =
        pcall(
        get.verified_name,
        format(
            "%s%s",
            name,
            var == "Twilight" and " (Twilight)" or var == "Lost Souls" and " (Lost Souls)" or
                var == "Superpig" and " (Superpig)" or
                ""
        )
    )
    if not ran then
        return format(
            '<span style="cursor:help; border-bottom:1px dotted; color: red;" title="Error: Card &quot;%s&quot; not found.">Error</span>[[Category:Pages with script errors]]',
            name or ""
        )
    end
    if var and not get.exists(format("%s (%s)", name, var)) then
        return format(
            '<span style="cursor:help; border-bottom:1px dotted; color: red;" title="Error: Variant &quot;%s&quot; not found.>Error</span>[[Category:Pages with script errors]]',
            var or ""
        )
    end

    local firstfile = ""
    if var == "Promo" then
        firstfile = "_(Promo)"
    elseif var == "Twilight" then
        firstfile = "_(Twilight)"
    elseif var == "Lost Souls" then
        firstfile = "_(Lost_Souls)"
    elseif var == "Superpig" then
        firstfile = "_(Superpig)"
    end
    local linkdest =
        args.nolink == true and "" or
        (name == "Mo" and "Mo#Card" or (var ~= "Promo" and format("%s%s", name, firstfile) or name))

    local size = math.floor(string.gsub(args.size or "20px", "px", "") + 0.5)

    local aff = false
    if not args["hide_affinity"] and (var == "Fire" or var == "Frost" or var == "Nature" or var == "Shadow") then
        aff = true
    end

    local linktext = name
    if args[2] ~= nil and args[2] ~= "" then
        linktext = args[2]
    elseif var then
        linktext = format("%s (%s)", linktext, var)
    end

    local u = args.u or args.upgrade or args.upgrades or args.applied_upgrade or args.applied_upgrades or 0
    local c = args.c or args.charge or args.charges or args.applied_charge or args.applied_charges or 0

    local span =
        mw.html.create("span"):addClass("card-icon custom-tooltip ci"):attr(
        "data-tt-type",
        args.show_upgrade and "upgrade" or "card"
    ):attr("data-1", args[1]):attr("data-2", u):attr("data-3", c)

    local span_icon =
        mw.html.create("span"):wikitext(
        format("[[File:%s%s_Card_Icon.png|%spx|border|link=%s]]", name, firstfile, size, linkdest)
    )

    if aff then
        span_icon:tag("span"):wikitext(
            format("[[File:Affinity_Orb_%s.png|%spx|link=%s]]", var, math.floor(size * 0.4 + 0.5), linkdest)
        )
    end

    span:node(span_icon:allDone())

    if not args.icononly then
        span:wikitext(format(" [[%s|%s]]", linkdest, linktext))
    end

    return span:done()
end

function p.infobox(frame)
    local args = getArgs(frame)

    local vname, _, _ = get.verified_name(args[1])

    return frame:expandTemplate {
        title = "Infobox card",
        args = {
            name = args[1],
            artwork = format("[[File:%s_Card_Artwork.png|link=]]", args[1]),
            card_type = get.type(vname) or nil,
            faction = get.faction(vname) == "Neutral" and "None (Neutral / Legendary)" or get.faction(vname),
            class = get.class(vname) or nil,
            has_normal = has.normal(args[1]) or nil,
            has_promo = has.promo(args[1]) or nil,
            affinity1 = has.affinities(args[1]) and get.affinity_variants(args[1])[1] or nil,
            affinity2 = has.affinities(args[1]) and get.affinity_variants(args[1])[2] or nil,
            power_cost = type(get.power_cost(vname)) == "table" and
                frame:expandTemplate {
                    title = "pu",
                    args = {
                        get.power_cost(vname)[1],
                        get.power_cost(vname)[2],
                        get.power_cost(vname)[3],
                        get.power_cost(vname)[4]
                    }
                } or
                get.power_cost(vname),
            orbs = get.orbs(vname) and concat(get.orbs(vname), ", ") or nil,
            charges = has.nonpromo(vname) and frame:expandTemplate {title = "pc", args = {get.charges(vname)}} or
                get.charges(vname),
            squadsize = get.squadsize(vname),
            damage_bonus = get.counter(vname),
            damage = type(get.damage(vname)) == "table" and
                frame:expandTemplate {
                    title = "pu",
                    args = {get.damage(vname)[1], get.damage(vname)[2], get.damage(vname)[3], get.damage(vname)[4]}
                } or
                get.damage(vname),
            size = get.size(vname),
            gender = get.gender(vname),
            booster = concat(get.boosters(vname), ", "),
            card_id = get.card_id(args[1]),
            card_id_fire = get.card_id(args[1] .. " (Fire)"),
            card_id_frost = get.card_id(args[1] .. " (Frost)"),
            card_id_nature = get.card_id(args[1] .. " (Nature)"),
            card_id_shadow = get.card_id(args[1] .. " (Shadow)"),
            card_id_promo = get.card_id(args[1] .. " (Promo)"),
            movement_speed = get.type(vname) == "Unit" and
                (type(get.movement_speed(vname)) == "table" and
                    frame:expandTemplate {
                        title = "pu",
                        args = {
                            get.movement_speed(vname)[1],
                            get.movement_speed(vname)[2],
                            get.movement_speed(vname)[3],
                            get.movement_speed(vname)[4]
                        }
                    } or
                    get.movement_speed(vname)) or
                nil,
            construction_time = get.type(vname) == "Building" and
                (type(get.construction_time(vname)) == "table" and
                    frame:expandTemplate {
                        title = "pu",
                        args = {
                            get.construction_time(vname)[1],
                            get.construction_time(vname)[2],
                            get.construction_time(vname)[3],
                            get.construction_time(vname)[4]
                        }
                    } or
                    get.construction_time(vname)) or
                nil,
            health = type(get.health(vname)) == "table" and
                frame:expandTemplate {
                    title = "pu",
                    args = {get.health(vname)[1], get.health(vname)[2], get.health(vname)[3], get.health(vname)[4]}
                } or
                get.health(vname),
            edition = get.edition(vname),
            rarity = get.rarity(vname)
        }
    }
end

--- returns a filtered list of cards
---@param frame table mw.frame object
---@return string list of cards as mw.html
---@public
function p.list(frame)
    local args = getArgs(frame)

    local list = get.list()
    local cardcount = 0
    local div =
        args.ci == true and mw.html.create() or
        mw.html.create("div"):addClass("list-of-cards" .. (args.filter and " collapsed" or "")):attr(
            "id",
            args.filter and "card-grid" or ""
        ):css {
            ["display"] = "flex",
            ["flex-wrap"] = "wrap",
            ["justify-content"] = "center",
            ["overflow"] = "hidden",
            ["max-height"] = args.filter and "500px" or nil,
            ["margin"] = "0 -1px"
        }

    if args[1] ~= "" and args[2] ~= "" and args[1] ~= "cardtype" then
        for i, k in ipairs(list) do
            if get[args[1]](k) == args[2] then
                cardcount = cardcount + 1
                if args.ci == true then
                    div:wikitext("\n* "):node(p.icon {k})
                else
                    div:node(p.card {k, filter = args.filter, scaling = 0.5487})
                end
            end
        end
    else
        if (args[1] == "cardtype" and args[2] == "all") or args[1] == nil or args[1] == "" then
            for _, k in ipairs(list) do
                cardcount = cardcount + 1
                div:node(p.card {k, filter = args.filter, scaling = 0.5487})
            end
        elseif (args[1] == "cardtype" and args[2] == "cards") or args[1] == nil or args[1] == "" then
            for _, k in ipairs(list) do
                if not k:find "(Promo)" then
                    cardcount = cardcount + 1
                    div:node(p.card {k, filter = args.filter, scaling = 0.5487})
                end
            end
        elseif (args[1] == "cardtype" and args[2] == "promos") or args[1] == nil or args[1] == "" then
            for _, k in ipairs(list) do
                if k:find "(Promo)" then
                    cardcount = cardcount + 1
                    div:node(p.card {k, filter = args.filter, scaling = 0.5487})
                end
            end
        end
    end

    if args.filter then
        local filterContainer =
            mw.html.create("div"):attr("id", "grid-filter-container"):cssText("background-color:#060e1f;padding:0.5rem")
        local arg1 = tostring(args[1] or ""):lower()
        local arg2 = tostring(args[2] or ""):lower()

        filterContainer:addClass("hide-" .. arg1)

        if arg1 == "faction" then
            if arg2 == "fire" then
                filterContainer:addClass("hide-frost hide-nature hide-shadow")
            elseif arg2 == "frost" then
                filterContainer:addClass("hide-fire hide-nature hide-shadow")
            elseif arg2 == "nature" then
                filterContainer:addClass("hide-fire hide-frost hide-shadow")
            elseif arg2 == "shadow" then
                filterContainer:addClass("hide-fire hide-frost hide-nature")
            elseif arg2 == "neutral" or arg2 == "legendary" then
                filterContainer:addClass("hide-fire hide-frost hide-nature hide-shadow")
            elseif arg2 == "amii" then
                filterContainer:addClass("hide-fire hide-frost")
            elseif arg2 == "bandit" then
                filterContainer:addClass("hide-nature hide-shadow")
            elseif arg2 == "lost souls" then
                filterContainer:addClass("hide-fire hide-nature")
            elseif arg2 == "stonekin" then
                filterContainer:addClass("hide-fire hide-shadow")
            elseif arg2 == "twilight" then
                filterContainer:addClass("hide-frost hide-shadow")
            end
        end

		-- omg the formatter f'ed up
        div:attr("data-card-count", tostring(cardcount)):tag("div"):attr("id", "grid-matches"):attr(
            "title",
            "If applicable, this count includes affinity and promo variants."
        ):wikitext((cardcount) .. " matching cards"):allDone()

        return filterContainer:tag("div"):attr("id", "grid-filter-buttons"):tag("div"):attr(
            "id",
            "grid-filter-orbs-wrapper"
        ):tag("div"):cssText("font-size: 15px; font-weight: 700"):wikitext("Orbs"):done():tag("div"):attr(
            "id",
            "grid-filter-orbs"
        ):cssText("display: flex"):done():done():tag("div"):attr("id", "grid-filter-orbsamount-wrapper"):tag("div"):cssText(
            "font-size: 15px; font-weight: 700"
        ):wikitext("Number of Orbs"):done():tag("div"):attr("id", "grid-filter-orbsamount"):cssText("display: flex"):done(

        ):done():tag("div"):attr("id", "grid-filter-type-wrapper"):tag("div"):cssText(
            "font-size: 15px; font-weight: 700"
        ):wikitext("Type"):done():tag("div"):attr("id", "grid-filter-type"):cssText("display: flex"):done():done():tag(
            "div"
        ):attr("id", "grid-filter-rarity-wrapper"):tag("div"):cssText("font-size: 15px; font-weight: 700"):wikitext(
            "Rarity"
        ):done():tag("div"):attr("id", "grid-filter-rarity"):cssText("display: flex"):done():done():done():tag("div"):attr(
            "id",
            "grid-filter-dropdowns"
        ):tag("div"):attr("id", "grid-filter-search"):attr("data-placeholder", "Search..."):done():tag("div"):attr(
            "id",
            "grid-filter-affinities"
        ):done():tag("div"):attr("id", "grid-filter-edition"):done():tag("div"):attr("id", "grid-filter-faction"):done():tag(
            "div"
        ):attr("id", "grid-filter-counter"):done():tag("div"):attr("id", "grid-filter-size"):done():tag("div"):attr(
            "id",
            "grid-filter-weapontype"
        ):done():tag("div"):attr("id", "grid-filter-special"):done():tag("div"):attr("id", "grid-filter-sort"):done():tag(
            "div"
        ):attr("id", "grid-filter-reset"):done():done():node(div):tag("div"):attr("id", "grid-collapse"):allDone()
    else
        return div:done()
    end
end

function p.map_drops(frame)
    local args = getArgs(frame)
    local mapData = mw.loadData("Module:Map/data")

    local res = get.map_drops(args[1], args[2])
    if args[3] == "list" then
        local d = ({"standard", "advanced", "expert"})[args[2]]
        local dt = mapData[args[1]].difficulties or {}
        if #res == 0 then
            if dt[d] then
                return format("No card upgrades as reward for this scenario on %s difficulty.", d)
            else
                return format("Scenario not available on %s difficulty.", d)
            end
        elseif not dt[d] then
            return format(
                '<span style="color:red;">Error: %s rewards found in [[Module:Card/data]], but %s isn\'t available on %s difficulty ([[Module:Map/data]]).</span>',
                #res,
                args[1],
                d
            )
        end
        local res2 = mw.html.create("ul")
        for i = 1, #res do
            res2:tag("li"):node(p.icon {res[i], show_upgrade = true, upgrade = args[2]}):done()
        end
        return res2
    elseif args[3] == "count" then
        return #res
    else
        return concat(res, ";")
    end
end

function p.navigation(frame)
    local list = get.list()
    local cardcount = 0
    local previous_card
    local incrH =
        (frame:getParent()["args"] and frame:getParent()["args"]["mainpage"] or
        frame:getParent()["args"]["increased-height"]) and
        true or
        false
    local height = incrH and "400px" or "230px"
    local grid =
        mw.html.create("div"):attr("id", "card-grid"):css {["max-height"] = height}:addClass(
        "lazyimg-wrapper collapsed" .. (incrH and " increased-height" or "")
    )

    for k, v in ipairs(list) do
        cardcount = k
        local name, _ =
            v:gsub(" %(Fire%)", ""):gsub(" %(Frost%)", ""):gsub(" %(Nature%)", ""):gsub(" %(Shadow%)", ""):gsub(
            " %(Promo%)",
            ""
        ):gsub(" %(Superpig%)", "")

        if name ~= previous_card then
            local data_orbs = get.orbs(v)
            local orbs = {data_orbs[1], data_orbs[2], data_orbs[3], data_orbs[4]}
            local non_legendary = (function()
                for _, v in ipairs(orbs) do
                    if v and v ~= "Neutral" then
                        return true
                    end
                end
            end)()

            local affinities = get.affinity_variants(name) or {}
            if affinities[1] then
                affinities[#affinities + 1] = "All"
            else
                affinities[1] = "None"
            end

            local cost = get.power_cost(v)
            local faction = get.faction(v)
            local faction = faction == "Neutral" and "Legendary" or faction

            local special = {}
            if has.promo(name) then special[#special+1] = 'Promo' end
        	if has.starter_card(name) then special[#special+1] = 'Starter' end
        	if has.normal(name) then special[#special+1] = 'Normal' end
            if non_legendary then special[#special+1] = "Non-Legendary" end


            grid:tag("span"):addClass("grid-icon custom-tooltip")
            :attr("data-tt-type", "card")
            :attr("data-1", name)
            :attr("data-search", name)
            :attr("data-rarity", get.rarity(v))
            :attr("data-cost", type(cost) == "table" and cost[1] or cost or 0)
            :attr("data-edition", concat(get.editions(name), ','))
            :attr("data-faction", faction)
            :attr("data-counter", get.counter(v))
            :attr("data-weapontype", get.weapon_type(v))
            :attr("data-special", concat(special, ','))
            :attr("data-orbs", concat(orbs, ","))
            :attr("data-orbsamount", #orbs)
            :attr("data-affinities", concat(affinities, ","))
            :attr("data-size", get.size(v))
            :attr("data-type", get.type(v))
            :wikitext(format("[[File:%s_Card_Icon.png|56px|alt=%s|link=%s]]", name, name, name))
            :done()
        end

        previous_card = name
    end

    grid:attr("data-card-count", tostring(cardcount)):tag("div"):attr("id", "grid-matches"):attr(
        "title",
        "If applicable, this count includes affinity and promo variants."
    ):wikitext((cardcount - 1) .. " matching cards"):allDone()
     -- minus QueekQueek (Superpig)

    return mw.html.create("div"):attr("id", "grid-filter-container"):tag("div"):attr("id", "grid-filter-buttons"):tag(
        "div"
    ):tag("div"):cssText("font-size: 15px; font-weight: 700"):wikitext("Orbs"):done():tag("div"):attr(
        "id",
        "grid-filter-orbs"
    ):cssText("display: flex"):done():done():tag("div"):tag("div"):cssText("font-size: 15px; font-weight: 700"):wikitext(
        "Number of Orbs"
    ):done():tag("div"):attr("id", "grid-filter-orbsamount"):cssText("display: flex"):done():done():tag("div"):tag(
        "div"
    ):cssText("font-size: 15px; font-weight: 700"):wikitext("Type"):done():tag("div"):attr("id", "grid-filter-type"):cssText(
        "display: flex"
    ):done():done():tag("div"):tag("div"):cssText("font-size: 15px; font-weight: 700"):wikitext("Rarity"):done():tag(
        "div"
    ):attr("id", "grid-filter-rarity"):cssText("display: flex"):done():done():done():tag("div"):attr(
        "id",
        "grid-filter-dropdowns"
    ):tag("div"):attr("id", "grid-filter-search"):attr("data-placeholder", "Search..."):done():tag("div"):attr(
        "id",
        "grid-filter-affinities"
    ):done():tag("div"):attr("id", "grid-filter-edition"):done():tag("div"):attr("id", "grid-filter-faction"):done():tag(
        "div"
    ):attr("id", "grid-filter-counter"):done():tag("div"):attr("id", "grid-filter-size"):done():tag("div"):attr(
        "id",
        "grid-filter-weapontype"
    ):done():tag("div"):attr("id", "grid-filter-special"):done():tag("div"):attr("id", "grid-filter-sort"):done():tag(
        "div"
    ):attr("id", "grid-filter-reset"):done():done():node(grid):tag("div"):attr("id", "grid-collapse"):allDone()
end

function p.progression_charges(frame)
    local args = getArgs(frame)
    if not args.index and not args[1] then
        return "Missing max charge value"
    end
    local rule = chargeRules(tonumber(args.index or args[1]))
    if not rule then
        return "Value " .. (args.index or args[1]) .. " not found in ruleset"
    end
    return frame:expandTemplate {
        title = "p",
        args = {
            [1] = rule[1],
            [2] = "No extra charges applied",
            [3] = rule[1] + rule[2],
            [4] = "1 extra charge applied",
            [5] = rule[1] + rule[2] + rule[3],
            [6] = "2 extra charges applied",
            [7] = rule[1] + rule[2] + rule[3] + rule[4],
            [8] = "3 extra charges applied",
            texttip = "true"
        }
    }
end

function p.upgrade(frame)
    local args = getArgs(frame)
    local upgrade = args.upgrade or args.cardupgrade or 0
    if upgrade == 0 or upgrade > 3 then
        return "upgrade needs to be 1, 2 or 3"
    end
    local name = args.name or args[1] or "Name missing"
    local display_name, aff, promo = verifyName(args.name or args[1] or "Name missing")

    local res = get.orbs(name) and get.factions(name, get.orbs(name)) or nil
    local factionLR = res and (res[1] ~= res[2] and res[1] .. res[2] or res[1]) or "Blank"
    local factionLeft = res and res[1] or nil
    local factionRight = res and res[2] or nil

    local description = mw.html.create("div")

    local general = get.general_upgrade_text(name)
    if general and (general[upgrade] or "") ~= "" then
        description:tag("div"):cssText("margin-left:5px;margin-right:10px;line-height:25px;white-space:pre-line"):wikitext(
            general[upgrade]
        ):done()
    end

    local abilities = get.abilities(name)
    local has_autocast = false
    for _, v in ipairs(abilities) do
        if v.type == "Autocast" then
            has_autocast = true
        end
    end

    local cost, dmg, hp = get.power_cost(name), get.damage(name), get.health(name)

    if type(cost) == "table" and cost[upgrade] ~= cost[upgrade + 1] then
        description:tag("div"):cssText("margin-left:5px;margin-right:10px;line-height:25px"):wikitext(
            (cost[upgrade + 1] > cost[upgrade] and "+" or "-") ..
                math.abs(cost[upgrade + 1] - cost[upgrade]) .. " power cost"
        ):done()
    end
    if not has_autocast and type(dmg) == "table" and dmg[upgrade] ~= dmg[upgrade + 1] then
        local s = get.squadsize(name) or 1
        local u =
            s ~= 1 and (tostring(math.abs(dmg[upgrade + 1] - dmg[upgrade]) / s) .. "x" .. tostring(s)) or
            math.abs(dmg[upgrade + 1] - dmg[upgrade])
        description:tag("div"):cssText("margin-left:5px;margin-right:10px;line-height:25px"):wikitext(
            "Damage " .. (dmg[upgrade + 1] > dmg[upgrade] and "+" or "-") .. u
        ):done()
    end
    if type(hp) == "table" and hp[upgrade] ~= hp[upgrade + 1] then
        local s = get.squadsize(name) or 1
        local u =
            s ~= 1 and (tostring(math.abs(hp[upgrade + 1] - hp[upgrade]) / s) .. "x" .. tostring(s)) or
            math.abs(hp[upgrade + 1] - hp[upgrade])
        description:tag("div"):cssText("margin-left:5px;margin-right:10px;line-height:25px"):wikitext(
            "Lifepoints " .. (hp[upgrade + 1] > hp[upgrade] and "+" or "-") .. u
        ):done()
    end

    for _, v in ipairs(abilities) do
        local from, to = 0, 9
        local at = v.upgrade_availability or {}
        if at[4] then
            from = 3
        end
        if at[3] then
            from = 2
        end
        if at[2] then
            from = 1
        end

        if at[3] and not at[4] then
            to = 3
        end
        if at[2] and not at[3] then
            to = 2
        end
        if at[1] and not at[2] then
            to = 1
        end

        local learned = from > 0 and upgrade == from
        local unlearned = upgrade == to

        local cost_change
        if not learned and not unlearned and type(v.cost) == "table" then
            if v.cost[upgrade] ~= v.cost[upgrade + 1] then
                cost_change =
                    (v.cost[upgrade + 1] < v.cost[upgrade] and "-" or "+") ..
                    math.abs(v.cost[upgrade + 1] - v.cost[upgrade])
            end
        end

        if (v.upgrade_text and (v.upgrade_text[upgrade] or "") ~= "") or learned or unlearned or cost_change then
            local abil = mw.html.create("div"):cssText("line-height:25px")
            local upgrade_count = 0
            local cost = v.cost
            local avai = type(v.upgrade_availability) == "table" and v.upgrade_availability or {true, true, true}
            if type(v.upgrade_text) == "table" or type(cost) == "table" then
                if
                    upgrade >= 1 and
                        (avai[1] == true and
                            ((type(v.upgrade_text) == "table" and (v.upgrade_text[1] or "") ~= "") or
                                (type(cost) == "table" and cost[1] ~= cost[2])))
                 then
                    upgrade_count = upgrade_count + 1
                end
                if
                    upgrade >= 2 and
                        (avai[2] == true and
                            ((type(v.upgrade_text) == "table" and (v.upgrade_text[2] or "") ~= "") or
                                (type(cost) == "table" and cost[2] ~= cost[3])))
                 then
                    upgrade_count = upgrade_count + 1
                end
                if
                    upgrade >= 3 and
                        (avai[3] == true and
                            ((type(v.upgrade_text) == "table" and (v.upgrade_text[3] or "") ~= "") or
                                (type(cost) == "table" and cost[3] ~= cost[4])))
                 then
                    upgrade_count = upgrade_count + 1
                end
            end

            if v.type then
                abil:addClass(
                    format(
                        "cv-abil cv-abil-%s%s",
                        string.lower(v.type),
                        v.affinity_dependency and "-" .. string.lower(aff) or ""
                    )
                )
            else
                abil:addClass("cv-abil-none")
            end
            abil:wikitext(v.name)
            if from < upgrade and not (learned or unlearned) then
                abil:tag("span"):cssText("margin-top:3px"):addClass(
                    format("cv-upgradeparts cv-abil-upgrade-%s", upgrade_count)
                ):done()
            end

            description:node(abil:done())

            if learned or unlearned then
                description:tag("div"):cssText("margin-left:5px;margin-right:10px;line-height:25px"):wikitext(
                    (learned and "+ " or "- ") .. v.name
                ):done()
            end

            if cost_change then
                description:tag("div"):cssText("margin-left:5px;margin-right:10px;line-height:25px"):wikitext(
                    cost_change .. " power cost"
                ):done()
            end

            if v.upgrade_text and (v.upgrade_text[upgrade] or "") ~= "" then
                description:tag("div"):cssText(
                    "margin-left:5px;margin-right:10px;line-height:25px;white-space:pre-line"
                ):wikitext(v.upgrade_text[upgrade]):done()
            end
        end
    end

    return CardClass {
        is_upgrade = true,
        scaling = args.scaling or args.displayscaling or nil,
        --TODO: notooltip = args.notooltip or false,
        notooltip = true,
        link = (args.nolink or link == "") and "" or display_name,
        full_name = display_name,
        display_name = display_name:gsub(" %(Lost Souls%)", ""):gsub(" %(Twilight%)", ""):gsub(" %(Superpig%)", ""),
        artwork_wktxt = format("[[File:%s_Card_Artwork.png||link=]]", display_name),
        factionLR = factionLR,
        factionLeft = factionLeft,
        factionRight = factionRight,
        affinity = aff,
        abilities_node = description:allDone(),
        cardupgrade = upgrade,
        upgrade_left_1_wktxt = format("[[File:%s_Upgrade_1_Left.png||link=]]", factionLeft),
        upgrade_left_2_wktxt = format("[[File:%s_Upgrade_2_Left.png||link=]]", factionLeft),
        upgrade_left_3_wktxt = format("[[File:%s_Upgrade_3_Left.png||link=]]", factionLeft),
        upgrade_right_1_wktxt = format("[[File:%s_Upgrade_1_Right.png||link=]]", factionRight),
        upgrade_right_2_wktxt = format(
            "[[File:%s_Upgrade_%s_Right.png||link=]]",
            factionRight,
            factionRight == "Neutral" and "1" or "2"
        ),
        upgrade_right_3_wktxt = format(
            "[[File:%s_Upgrade_%s_Right.png||link=]]",
            factionRight,
            factionRight == "Neutral" and "1" or "3"
        )
    }.build()
end

return p