Module:List

From Outward Wiki
Jump to navigation Jump to search
Template-info.svg Documentation

Module used to list Items.

Arguments

There are three different functions you can invoke with this module. All can use the main arguments:

Filter arguments:

  • category - either "Item", "Equipment", "Armor", "Weapon" or "Consumable". Defaults to "Item".
  • class - the class of the Item, valid for Equipment, Armor and Weapons.
  • type - the type of the item, valid for Weapons.
  • set - the item set

Output control arguments:

  • default - if true, will show default text when no results are found, otherwise will show nothing.
  • showClass - if true it will show the class as a column in the table, defaults to false
  • showDurability - if true it will show durability as a column in the table, defaults to true

Main function

The function items will return a list of all items which match the provided filter.

It has no extra arguments other than the main arguments.

  • Example: {{#invoke:List|items|category=Weapon|class=Axe|type=One-Handed}}

Manual function

The function manual will list a manually supplied list of items through the argument itemsToList.

  • Example: {{#invoke:List|manual|category=Weapon|itemsToList=Iron Sword,Tsar Axe,Lexicon}}

Element function

The function element will list all items which have damage, damage bonus or damage resistance for the provided element argument.

  • Example: {{#invoke:List|element|element=Fire}}

Test

{{#invoke:List|items|category=Armor|set=Tsar Set}}

IconNameResistancesImpact.pngProtectionHotWeather.pngStamina CostMana CostMovementDurabilityWeight
Tsar Armor.pngTsar Armor28% Physical
40% Fire
23% Impact.pngProtection10 HotWeather.png6% Stamina Cost-11% Movement23.0
Tsar Boots.pngTsar Boots20% Physical
20% Fire
13% Impact.pngProtectionHotWeather.png4% Stamina Cost-9% Movement17.0
Tsar Helm.pngTsar Helm20% Physical
20% Fire
13% Impact.pngProtectionHotWeather.png4% Stamina Cost15% Mana Cost-9% Movement13.0

local p = {}

category = "Item"
class = ""
itemType = ""
itemset = ""
default = true
showClass = false
showDurability = true
collapsible = false
collapseID = 0

-- the fields used for the Cargo query. Not all necessarily used as columns.
itemFields = '_pageName,name,image,class,type,DLC,effects,weight,buy,sell,durability,protection,barrier,statusResist,physical,ethereal,'
 .. 'decay,lightning,frost,fire,impact,physicalBonus,etherealBonus,decayBonus,lightningBonus,frostBonus,fireBonus,manaCostReduction,'
 .. 'staminaCostReduction,movespeed,coldresist,heatresist,pouch,itemset,corruptresist,cooldown,stamcost,attackspeed,impactResist,capacity,'
 .. 'preservation,inventProt,thirst,hunger,reach'


-- the column headers used for the output table.
-- Key: field name
-- Value: displayed column header
-- NOTE: If you add a column to this, make sure you also add it to the 'columnsIndexed' table below as well.
-- This table does NOT determine the order of the columns, columnsIndexed does.
columnHeaders = {
    image='Icon',
    name='Name',
    damage='Damage',
    resistance='Resistances',
    impact='{{impact}}',
    impactResist='{{impact}} Resist',
    protection='{{protection}}',
    barrier='{{barrier}}',
    statusResist='{{statusResist}}',
    reach='Reach',
    attackspeed='Speed',
    stamcost='{{Stamina}}',
    bonus='Damage Bonus%',
    thirst='Thirst',
    hunger='Hunger',
    effects='Effects',
    heatresist='{{HotWeather}}',
    coldresist='{{ColdWeather}}',
    staminaCostReduction='{{Stamina Cost}}',
    corruptresist='{{corruptionResist}}',
    cooldown='{{cooldown}}',
    manaCostReduction='{{Mana Cost}}',
    movespeed='{{Movement}}',
    preservation='Preservation',
    inventProt='Inventory Protection',
    durability='Durability',
    weight='Weight',
    capacity='Capacity',
    pouch='Pouch Bonus',
    class='Class'
}

-- Determines the order of the columns. All keys of "columnHeaders" should be here.
-- we could instead have a table of tables to keep the index order, but just gonna leave it like this for now.
columnsIndexed = {
    'image','name','damage','resistance','impact','impactResist','protection','barrier',
    'statusResist','reach','stamcost','attackspeed','bonus','thirst','hunger','heatresist','coldresist','staminaCostReduction','manaCostReduction',
    'corruptresist','cooldown','movespeed','pouch','capacity','durability','weight','effects','preservation','inventProt','class'
}

-- helper tables for the 6 elements since they are iterated over a few times
elementsIndexed = { 'physical', 'ethereal', 'decay', 'lightning', 'frost', 'fire' }
elements = { 
    physical = '{{phys}}', 
    ethereal = '{{ethereal}}', 
    decay = '{{decay}}', 
    lightning = '{{lightning}}', 
    frost = '{{frost}}', 
    fire = '{{fire}}' 
}

-- some CSS styles
local styles = {
    default = 'text-align: center; vertical-align: top',
    red = 'text-align: center; vertical-align: top; color: #df756f',
    green = 'text-align: center; vertical-align: top; color: #aedc99'
}

-- quick lookup for red/green colored values. Bool value is for %-based values
local redGreenValue = {
    movespeed=true, staminaCostReduction=true, manaCostReduction=true,
    preservation=true, cooldown=true, corruptresist=true, statusResist=true,
    impactResist=true,heatresist=false,coldresist=false
}

-- cells which want an icon after the displayed value
local iconCells = {
    impact='{{impact}}',
    impactResist='{{impact}}',
    protection='{{protection}}',
    barrier='{{barrier}}',
    statusResist='{{statusResist}}',
    attackspeed='{{attack Speed}}',
    stamcost='{{Stamina}}',
    heatresist='{{HotWeather}}',
    coldresist='{{ColdWeather}}',
    staminaCostReduction='{{Stamina Cost}}',
    corruptresist='{{corruptionResist}}',
    cooldown='{{cooldown}}',
    manaCostReduction='{{Mana Cost}}',
    movespeed='{{Movement}}',
}

-- helper to return when no results are found
function p.defaultReturn()
    if default == true then
        return '<span style="color:#e77">\'\'No items found.\'\'</span>'
    else 
        return ''
    end
end

-------------------- Main Function -------------------

-- 'items' is the main function for listing all items of the provided 'category' (defaults to 'Item').
-- can filter further with 'class', 'itemType' and 'set' arguments.
function p.items(frame)
    args = require('Module:ProcessArgs').merge(true)

    category = args.category or "Item"
	class = args.class or ""
	itemType = args.type or ""
	itemset = args.set or ""
    default = args.default or true
    showClass = args.showClass or args.showclass or false
	showDurability = args.showDurability or args.showdurability or true
	collapsible = args.collapsible or false
	collapseID = args.collapseID or collapseID

    local result = p.doQuery(args)
    if not result then
		return p.defaultReturn()
	end
	
	local html = ''
	-- if collapsible, add the button
	if collapsible then
	    html = '<div class="mw-customtoggle-' .. collapseID .. ' wds-button wds-is-secondary">Show/Hide table</div>'
    end
    
    html = html .. tostring(p.formatResult(result, frame, nil))
    return html
end

function p.doQuery(args)
	local cargoArgs = {
        where = p.cargoWhere(args),
		orderBy = 'name',
		groupBy = 'name',
		limit = 10000
	}
	return mw.ext.cargo.query('ItemData', itemFields, cargoArgs)
end

function p.cargoWhere(args)
	local where = 'category="' .. category .. '"'
    if not p.isempty(class) then
        where = where .. ' AND class="' .. class .. '"'
    end
    if not p.isempty(itemType) then
        where = where .. ' AND type="' .. itemType .. '"'
    end
    if not p.isempty(itemset) then
        where = where .. ' AND itemset="' .. itemset .. '"'
    end
    if not p.isempty(args.where) then
    	where = where .. ' AND (' .. args.where .. ')'	
	end
    return where
end

------------------- Element function -------------------

-- 'element' lists all items with the provided element argument
-- example: {{#invoke:list|element|element=ethereal}}
function p.element(frame)
    args = require('Module:ProcessArgs').merge(true)

    showClass = true
	showDurability = false
	collapsible = args.collapsible or true

	local element = args.element
	local elementBonus = element .. 'Bonus'

	local html = mw.html.create()

	category='Weapon'
	p.elementQuery('Weapon', element, elementBonus, html, frame)
	html:newline()
	category='Armor'
	p.elementQuery('Armor', element, elementBonus, html, frame)
	html:newline()
	category='Equipment'
	p.elementQuery('Equipment', element, elementBonus, html, frame)
	
	return html
end

-- query for the 'element' function
function p.elementQuery(category, element, elementBonus, html, frame)
	-- do the query for this category and element
	local cargoWhere = element .. ' IS NOT NULL OR ' .. elementBonus .. ' IS NOT NULL' 
    local cargoArgs = {
        where = '(category="' .. category .. '") AND (' .. cargoWhere .. ')',
        orderBy = 'name',
        groupBy = 'name',
        limit = 5000
    }
	local result = mw.ext.cargo.query('ItemData', itemFields, cargoArgs)
	-- if results, append to html
	if (result ~= nil and #result > 0) then
		-- create the header for the results
	    html:wikitext(frame:preprocess('===' .. category .. 's===')):newline()
	    -- if collapsible, add the button
	    if collapsible then
	    	html:wikitext('<div class="mw-customtoggle-' .. collapseID .. ' wds-button wds-is-secondary">Show/Hide table</div>')
    	end
	    -- use the normal table builder
	    local processedResult = p.formatResult(result, frame, element)
	    -- append the table to our html
	    html:node(processedResult)
	end
end

------------------ Manual function -----------------

-- 'manual' function for listing a known list of items. 
-- example: {{#invoke:list|manual|itemsToList=Item1,Item2,Item3}}
function p.manual(frame)
    args = require('Module:ProcessArgs').merge(true)

    showClass = true
	showDurability = true
	category = args.category or 'Item'
	default = false
	collapsible = args.collapsible or false

	local items = p.split(args.itemsToList, ',')

	local cargoWhere = ''
	for _, v in ipairs(items) do
		if cargoWhere ~= '' then cargoWhere = cargoWhere .. ' OR ' end
		cargoWhere = cargoWhere .. '_pageName="' .. v .. '"'
	end

    local cargoArgs = {
        where = '(category="' .. category .. '") AND (' .. cargoWhere .. ')',
        orderBy = 'name',
        groupBy = 'name',
        limit = 5000
    }
	local result = mw.ext.cargo.query('ItemData', itemFields, cargoArgs)

	if result == nil or #result < 1 then
		return p.defaultReturn()
	else
		return p.formatResult(result, frame, nil)
	end
end

-------------------------------------------------------------------------------
---------------------------- Formatting the result ----------------------------

--------------------------
-- Main output function --
--------------------------
function p.formatResult(result,frame,element) 
    -- create a table of flags to determine which columns are actually used
    local columnUsedFlags = {}
    for field,_ in pairs(columnHeaders) do
        columnUsedFlags[field] = false
    end

    -- fill the columns with the item data
    processed = {}
    for i, row in pairs(result) do
    	if (string.find(row._pageName, "Module:") == nil and string.find(row._pageName, "Template:") == nil) then
        	processed[i] = p.processRow(columnUsedFlags, row, frame)
    	end
    end

    -- check showDurabilty and showClass overrides
	if not showDurability then
		columnUsedFlags['durability'] = false
	end
    if not showClass then
        columnUsedFlags['class'] = false 
    end

    -- remove 'attackspeed' for off-hands as it is redundant
    if class == "Shield" or class == "Chakram" or class == "Dagger" or class == "Pistol" then
        columnUsedFlags['attackspeed'] = false
        columnUsedFlags['reach'] = false
    end

    -- remove 'reach' for weapons that don't have it
    if class == "Bow" or class == "Chakram" or class == "Pistol" then
        columnUsedFlags['reach'] = false
    end

    -- format the output with table headers and the result data
    return p.makeTable(processed, columnUsedFlags, frame, element)
end

-----------------------------------------------------------
-- Process a result from the query into our output table --
-----------------------------------------------------------
-- This just has some processing for Resistances, Damage and Damage Bonuses.
-- For everything else, it just copies the value and sets the flag to true.
function p.processRow(columnUsedFlags, row, frame)
    processedRow = {}
    -- process each column for the result
    for _,column in ipairs(columnsIndexed) do
        -- total Resistances, Weapon Damage and Damage Bonus are combined into their own columns
        if (column == 'damage' and category ~= 'Set' and category ~= 'Armor') 
        or (column == 'resistance' and (category == 'Set' or category == 'Armor'))
        or column == 'bonus' then
            local sum = 0
            local any = false
            local formatted = {}
            -- iterate over each element and add it to the formatted table if there is a defined value
            for element,icon in pairs(elements) do
                local eleKey = element
                if column == 'bonus' then eleKey = eleKey .. 'Bonus' end
                if not p.isempty(row[eleKey]) then
                    any = true
                    -- format the output
                    local processed = '{{#expr:' .. row[eleKey] .. '}}'
                    if column ~= 'damage' then processed = processed .. '%' end
                    processed = processed .. '&nbsp;' .. icon .. '<br>'
                    formatted[element] = processed
                    -- add the sum
                    sum = sum + row[eleKey]
                    -- for element data-sort-value
                    processedRow[eleKey] = row[eleKey]
                end
            end
            -- declare the sum value for data-sort
            processedRow[column .. 'Sum'] = sum
            -- if there is any defined, format the total output
            if any then
                columnUsedFlags[column] = true
                processedRow[column] = ''
                for _,type in ipairs(elementsIndexed) do
                    local val = formatted[type] or ''
                    processedRow[column] = processedRow[column] .. val
                end
            else
                processedRow[column] = '–'
                processedRow[column .. 'Sum'] = 0
            end
        else -- everything else is just defined automatically like so
            if not p.isempty(row[column]) then
                columnUsedFlags[column] = true
                processedRow[column] = row[column]
            else
                processedRow[column] = '–'
            end
        end
    end

    -- values needed for formatting but not included as headers
    processedRow._pageName = row._pageName
    processedRow.type = row.type
    processedRow.DLC = row.DLC

	return processedRow
end

----------------------------------------------------------
-- Turn the processed results into an actual wiki table --
----------------------------------------------------------
function p.makeTable(processed, columnUsedFlags, frame, element)
    -- make the output table
    local tbl = mw.html.create('table'):addClass('wikitable'):addClass('sortable')
    
    if collapsible then
    	tbl = tbl:addClass('mw-collapsible article-table')
    			 :attr('id', 'mw-customcollapsible-' .. collapseID)
    	collapseID = collapseID + 1
	end

    --for armor and set, 'impact' is used as impactResist.
    if category == 'Armor' or category == 'Set' then
        redGreenValue.impact = true
    end

    -- add headers
	local headerRow = tbl:tag('tr')
	
    for _,k in ipairs(columnsIndexed) do
		if columnUsedFlags[k] == true then
            if k == "damage" or k == "bonus" then
                headerRow:tag('th'):css('width:70px'):wikitext(frame:preprocess(columnHeaders[k]))
            else
                headerRow:tag('th'):wikitext(frame:preprocess(columnHeaders[k]))
            end
        end
	end
	--if category == "Set" then
	--	headerRow:tag('th'):addClass('nomobile'):wikitext('Set items')
	--end

    -- fill table rows
    for i,row in pairs(processed) do
        -- add a row
        tr = tbl:tag('tr')
        -- iterate over each column header
        for _,column in ipairs(columnsIndexed) do
            if columnUsedFlags[column] == true then
                -- add a cell
                td = tr:tag('td')
                -- format the row
                p.addRow(column, row, td, frame, element)
            end
        end
        ---- the "Show Set Items" button for sets
        --if category == "Set" then
        --    tr:tag('td')
        --        :addClass(string.format('mw-customtoggle-%s nomobile',i))
        --        :wikitext('<div style="background-color: rgba(225,215,127,0.2); font-weight:bold; text-align: center; '
        --            .. 'display:table; height:80px; width:80px"><span style="vertical-align:middle; display:table-cell">Show</span></div>')
        --    tbl:tag('tr')
        --        :addClass('expand-child mw-collapsible mw-collapsed nomobile')
        --        :attr('id', string.format('mw-customcollapsible-%s',i))
        --        :tag('td')
        --        :attr('colspan', 19)
        --        :wikitext(frame:expandTemplate{title="set items", args = {set=row.name}})
        --end
    end

    return tbl
end

--------------------------------------------------------
-- Format a result and add a td-row to our html table --
--------------------------------------------------------
function p.addRow(column, row, td, frame, element)
    -- the 'wikitext' value for the td will be defined by our 'output' variable.
    local output = ''
    -- css styles
    local style = styles.default
    -- cached tonumber since we use it a bit
    local valAsNum = tonumber(row[column])

    -- image
    if column == 'image' then
        local image = row.image or 'Placeholder.png'
        output = string.format("[[File:%s|35x53px|link=%s]]", image, row._pageName)
    -- displayed name/link
    elseif column == 'name' then
        local formattedName = '[[' .. row._pageName .. '|' .. row.name .. ']]'
        if not p.isempty(row.DLC) then
            formattedName = frame:preprocess(formattedName .. ' {{' .. row.DLC .. '}}')
        end
        output = formattedName
    -- class
    elseif column == 'class' then
        if (row.class == "Shield" or row.class == "Bow" or row.class == "Spear") then
            output = string.format("[[%ss|%s]]", row.class, row.class)
        elseif(row.type == "One-Handed") then
            output = string.format("[[%ss#%s|1H %s]]", row.class, row.type, row.class)
        elseif (row.type == "Two-Handed") then
            output = string.format("[[%ss#%s|2H %s]]", row.class, row.type, row.class)
        elseif (row.type == "Staff" or row.type == "Halberd") then
            output = string.format("[[%ss|%s]]", row.class, row.type)
        elseif (row.class == "Chest") then
            output = "[[Body Armor]]"
        elseif (row.class == "Legs") then
            output = "[[Boots]]"
        elseif (row.class == "Head") then
            output = "[[Helmets]]"
        elseif (p.isempty(row.class)) then
            if (row.type == "Other") then
                output = "Other"
            else
                output = string.format("[[%ss|%s]]", row.type, row.type)
            end
        else
            output = string.format("[[%ss|%s]]", row.class, row.class)
        end
    -- hunger and thirst
    elseif column == "hunger" or column == "thirst" then
        if (valAsNum ~= nil) then
            output = tostring(valAsNum / 10) .. '%'
            if     valAsNum > 0 then style = styles.green
            elseif valAsNum < 0 then style = styles.red
            end
        else
            output = '–'
        end
    -- %-based values with red/green formatting
    elseif redGreenValue[column] ~= nil then
        if (valAsNum ~= nil) then
            output = string.format("%.0f", valAsNum)
            if redGreenValue[column] == true then
                output = output .. '%'
            end
            -- for these values, negative is good and positive is bad
            if column == 'manaCostReduction' or column == 'staminaCostReduction' then
                if     valAsNum < 0 then style = styles.green
                elseif valAsNum > 0 then style = styles.red
                end
            else
                if     valAsNum < 0 then style = styles.red
                elseif valAsNum > 0 then style = styles.green
                end
            end
        else
            output = '–'
        end
    -- weight and attack speed (always decimal)
    elseif column == 'weight' or column == 'attackspeed' then
        if valAsNum ~= nil then
            if (column == 'attackspeed' and row[column] == "1") then
                row[column] = "1.0"
            end
            output = string.format("%.1f", row[column])
        else
            output = '–'
        end
    else
        output = row[column]
    end

    -- check if we want an icon after the value
    if output ~= '–' and not p.isempty(output) then
        local cellIcon = iconCells[column]
        if cellIcon ~= nil then
            output = output .. '&nbsp;' .. cellIcon
        end
    end

    -- data sort value
    local dataSortValue = valAsNum or 0
    -- resistances, damage and damage bonus use a special data-sort-value
    if column == 'damage' or column == 'bonus' or column == 'resistance' then
        local sum = 0
        -- if a specific element was defined, use only that element to sort.
        if element ~= nil then
            if column == 'damage' or column == 'resistance' then
                sum = row[element]
            elseif column == 'bonus' then
                sum = row[element .. 'Bonus']
            end
        else
            -- use the sum we defined in processRow
            sum = row[column .. 'Sum']
        end
        dataSortValue = tonumber(sum) or 0
    end
	
	if column == 'name' or column == 'effects' then
		dataSortValue = row[column]
	end

	-- if food durability, calculate data-sort-value
	if (column == 'durability' and (string.find(row[column], 'Day') ~= nil or string.find(row[column], 'Hour') ~= nil)) then
		dataSortValue = 0
		local digit, _ = string.match(row[column], "(%d+)( Day)")
		dataSortValue = dataSortValue + ((tonumber(digit) or 0) * 24)
		digit, _ = string.match(row[column], "(%d+)( Hour)")
		dataSortValue = dataSortValue + (tonumber(digit) or 0)
	elseif (string.find(row[column], '∞') ~= nil) then
		dataSortValue = 999999
	end
	
    -- actually add our output and data-sort-value
    td:attr('data-sort-value', dataSortValue)
    td:cssText(style)
    -- for everything except name and effects, make whitespace no-wrap
    if column ~= 'name' and column ~= 'effects' then
        td:cssText('white-space: nowrap; ')
    end
    td:wikitext(frame:preprocess(output))

end


------------- HELPERS --------------

-- removes the key from the table if it was found
function p.removeIfFound(table, key)
    local index = p.tablefind(table, key)
    if (index ~= -1) then
        table.remove(table, index)
    end
end

-- searches the table for a key. if found, returns the index of that key, otherwise returns -1.
function p.tablefind(table,key)
    for index, entry in pairs(table) do
        if entry == key then
            return index
        end
	end
	return -1
end

-- returns true if the provided object is nil, blank, 0 or false, otherwise returns false.
function p.isempty(s)
	return s == nil or s == '' or s == 0 or s == false
end

-- splits the input string into a table by the provided seperator
function p.split(inputstr, sep)
    if sep == nil then
        sep = "%s"
    end
    local t={}
    if p.notempty(inputstr) then
        for str in string.gmatch(inputstr .. ',', "([^"..sep.."]+)") do
            table.insert(t, str)
        end
    end
    return t
end

-- searches the provided table for a key. if found, returns that key's value.
function p.find(tbl, val)
    for k, v in pairs(tbl) do
        if k == val then return v end
    end
    return nil
end

-- returns true if the string does not equal nil or '', otherwise returns false.
function p.notempty(string)
    return string ~= nil and string ~= ''
end

-- returns true if any value in table1 matches a value in table2
function p.hasmatch(table1, table2)
    for _,v in ipairs(table1) do
        if contains(table2, v) then
            return true
        end
    end
    return false
end

-- returns true if any value in the table matches the provided value
function p.contains(table, value)
    for _,v in ipairs(table) do
        if v == value then
            return true
        end
    end
    return false
end

return p