Module:Sandbox/balaar/item acquisition: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
(Sandbox for testing changes to drop text to incorporate reference to iLvl restrictions and when available at vendor)
 
(Added various variables and conditionals to allow for better item acquisition information and vendor availability)
Line 22: Line 22:
-- The cfg table contains all localisable strings and configuration, to make it
-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
-- easier to port this module to another wiki.
local cfg = use_sandbox and mw.loadData('Module:Item acquisition/config/sandbox') or mw.loadData('Module:Item acquisition/config')
local cfg = mw.loadData('Module:Item acquisition/config/sandbox/balaar')


local i18n = cfg.i18n
local i18n = cfg.i18n
Line 408: Line 408:
         'items.drop_enabled',
         'items.drop_enabled',
         'items.is_drop_restricted',
         'items.is_drop_restricted',
        'items.drop_level', --added to allow for better information where item can drop based on iLvl
        'items.class_id', --added to prevent issues with div/skill gem cards gaining char-level based acquistion from vendor text
     }
     }
     local query = {
     local query = {
Line 438: Line 440:
     local drop_restricted = m_util.cast.boolean(item_data['items.is_drop_restricted'])
     local drop_restricted = m_util.cast.boolean(item_data['items.is_drop_restricted'])
     local is_unique = item_data['items.rarity_id'] == 'unique'
     local is_unique = item_data['items.rarity_id'] == 'unique'
    local is_sold_at_vendor = item_data['items.class_id'] ~= 'DivinationCard'
    local is_skill = item_data['items.class_id'] == 'Active Skill Gem' or 'Support Skill Gem'
     if not drop_enabled then
     if not drop_enabled then
         out[#out+1] = string.format(i18n.acquisition.drop_disabled, item_data['items.name'])
         out[#out+1] = string.format(i18n.acquisition.drop_disabled, item_data['items.name'])
Line 448: Line 452:
     else
     else
         if not item_data['items.drop_areas__full'] then
         if not item_data['items.drop_areas__full'] then
             out[#out+1] = string.format(i18n.acquisition.drop_anywhere, item_data['items.name'])
             out[#out+1] = string.format(i18n.acquisition.drop_anywhere, item_data['items.name'], item_data['items.drop_level'])
         end
         end
         if is_unique then
         if is_unique then
             out[#out+1] = ' '
             out[#out+1] = ' '
             out[#out+1] = i18n.acquisition.can_be_chanced
             out[#out+1] = i18n.acquisition.can_be_chanced
        elseif is_sold_at_vendor and not is_skill then
        out[#out+1] = ' '
        out[#out+1] = string.format(i18n.acquisition.can_be_purchased, item_data['items.name'], item_data['items.drop_level'] - 1)
         end
         end
     end
     end

Revision as of 17:03, 26 September 2021

Module documentation[create] [purge]
-------------------------------------------------------------------------------
-- 
--           Balaar Sandbox for  Module:Item acquisition
-- 
-- This module implements Template:Item acquisition.
-------------------------------------------------------------------------------

require('Module:No globals')
local getArgs = require('Module:Arguments').getArgs
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')
local m_quest_reward = require('Module:Quest reward')._shared

local m_game = mw.loadData('Module:Game')

-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Item acquisition')

-- Lazy loading
local f_item_link -- require('Module:Item link').item_link

-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = mw.loadData('Module:Item acquisition/config/sandbox/balaar')

local i18n = cfg.i18n

-- ----------------------------------------------------------------------------
-- Helper functions
-- ----------------------------------------------------------------------------

local h = {}

-- Lazy loading for Module:Item link
function h.item_link(args)
    if not f_item_link then
        f_item_link = require('Module:Item link').item_link
    end
    return f_item_link(args)
end

function h.head(str, level)
    local head = mw.html.create('h' .. (level or 3))
    head:wikitext(str)
    return tostring(head)
end

function h.fetch_upgraded_from_sets(where_set, where_group)
    if where_set == "" or where_group == "" then
        return {}, {}
    end
    
    local sets = {}
    local results = m_cargo.query(
        {'upgraded_from_sets'},
        {
            'upgraded_from_sets._pageName',
            'upgraded_from_sets.set_id',
            'upgraded_from_sets.text',
            'upgraded_from_sets.automatic',
        },
        {
            where=where_set,
        }
    )
    
    for _, row in ipairs(results) do
        row.groups = {}
        sets[row['upgraded_from_sets._pageName'] .. tonumber(row['upgraded_from_sets.set_id'])] = row
    end
    
    results = m_cargo.query(
        {'upgraded_from_groups'},
        {
            'upgraded_from_groups._pageName',
            'upgraded_from_groups.set_id',
            'upgraded_from_groups.group_id',
            'upgraded_from_groups.notes',
            'upgraded_from_groups.amount',
            'upgraded_from_groups.item_name',
            'upgraded_from_groups.item_page',
        },
        {
            where=where_group,
        }
    )
    
    for _, row in ipairs(results) do
        sets[row['upgraded_from_groups._pageName'] .. tonumber(row['upgraded_from_groups.set_id'])].groups[tonumber(row['upgraded_from_groups.group_id'])] = row
    end
    
    local sets_sort = {}
    for key, _ in pairs(sets) do
        sets_sort[#sets_sort+1] = key
    end
    
    table.sort(sets_sort, function (a, b)
        return tonumber(sets[a]['upgraded_from_sets.set_id']) < tonumber(sets[b]['upgraded_from_sets.set_id'])
    end)
    
    return sets, sets_sort
end

function h.upgraded_from_table(tpl_args, sets, set_order, data_type, out, item_pages)
    
    -- Count the number of item pages:
    local item_pages_count = 0
    for _,__ in pairs(item_pages) do 
        item_pages_count = item_pages_count + 1
    end
    
    -- Avoid creating headers only when no data is given
    if #set_order <= 0 then
        return
    end
    
    -- Prevent showing either group or set notes when no rows have any to save a bit of vertical space
    local set_notes = false
    local group_notes = false
    for _, set_key in ipairs(set_order) do
        if set_notes and group_notes then
            break
        end
        local set = sets[set_key]
        if set['upgraded_from_sets.text'] ~= nil then
            set_notes = true
        end
        
        if #set.groups > 0 then
            for i, group in ipairs(set.groups) do
                if group['upgraded_from_groups.notes'] ~= nil then
                    group_notes = true
                    break
                end
            end
        end
    end
    
    local tbl = mw.html.create('table')
    
    -- sorting will mess up the table because of the rowspawns
    local table_class = 'wikitable mw-collapsible mw-expanded'
    if item_pages_count > cfg.COLLAPSE_TABLE then
        table_class = 'wikitable mw-collapsible mw-collapsed'
    end
    tbl:attr('class', table_class)
    
    --
    -- Header
    -- 
    local tr = tbl:tag('tr')
    if data_type == 'ingredient' then
        tr
            :tag('th')
                :wikitext(i18n.upgraded_from_table.outcome)
    end
    
    tr
        :tag('th')
            :wikitext(i18n.upgraded_from_table.ingredient_amount)
            :done()
        :tag('th')
            :wikitext(i18n.upgraded_from_table.ingredient)
            :done()
    if group_notes then
        tr
            :tag('th')
                :wikitext(i18n.upgraded_from_table.ingredient_notes)
                :done()
    end
    if set_notes then
         tr
            :tag('th')
                :wikitext(i18n.upgraded_from_table.recipe_notes)
                :done()
    end
    tr
        :tag('th')
            :wikitext(i18n.upgraded_from_table.type)
            :done()
    --
    -- Rows
    --
    
    for _, set_key in ipairs(set_order) do
        local set = sets[set_key]
        
        if #set.groups > 0 then
            tr = tbl:tag('tr')
            tr:attr('class', 'upgraded-from-set')
            
            if data_type == 'ingredient' then
                local str
                if tpl_args.no_link == nil and item_pages_count < cfg.MAX_ITEMS then
                    local item_data = item_pages[set['upgraded_from_sets._pageName']][1]
                    str = h.item_link{page=set['upgraded_from_sets._pageName'], name=item_data['items.name'], inventory_icon=item_data['items.inventory_icon'] or '', html=item_data['items.html'] or '', skip_query=true}
                else
                    str = string.format('[[%s]]', set['upgraded_from_sets._pageName'])
                end
                
                tr
                    :tag('td')
                        :attr('rowspan', #set.groups)
                        :wikitext(str)
                        :done()
            end
            
            local tr2
            for i, group in ipairs(set.groups) do
                if i <= 1 then
                    tr2 = tr
                else
                    tr2 = tbl:tag('tr')
                end
                local str
                if tpl_args.no_link == nil and item_pages_count < cfg.MAX_ITEMS then
                    local item_data = item_pages[group['upgraded_from_groups.item_page']][1]
                    str = h.item_link{
                        page=group['upgraded_from_groups.item_page'], 
                        name=item_data['items.name'], 
                        inventory_icon=item_data['items.inventory_icon'] or '', 
                        html=item_data['items.html'] or '', 
                        skip_query=true
                    }
                else
                    str = string.format('[[%s]]', group['upgraded_from_groups.item_page'])
                end
                
                tr2
                    :tag('td')
                        :attr('data-sort-type', 'number')
                        :wikitext(group['upgraded_from_groups.amount'])
                        :done()
                    :tag('td')
                        :wikitext(str)
                        :done()
                
                if group_notes then
                    if group['upgraded_from_groups.notes'] then
                        tr2
                            :tag('td')
                                :wikitext(group['upgraded_from_groups.notes'])
                    else
                        tr2:node(m_util.html.td.na{as_tag=true})
                    end
                end
            end
            
            if set_notes then
                if set['upgraded_from_sets.text'] then
                    tr
                        :tag('td')
                            :attr('rowspan', #set.groups)
                            :wikitext(set['upgraded_from_sets.text'])
                else
                    tr
                        :tag('td')
                            :attr('class', 'table-na')
                            :attr('rowspan', #set.groups)
                            :wikitext('N/A')
                            :done()
                end
            end
            
            local t
            if set['upgraded_from_sets.automatic'] == nil then
                t = i18n.upgraded_from_table.old_data
            elseif set['upgraded_from_sets.automatic'] == '1' then
                t = i18n.upgraded_from_table.automatic
            else
                t = i18n.upgraded_from_table.manual
            end
            
            tr
                :tag('td')
                    :attr('rowspan', #set.groups)
                    :wikitext(t)
        end
    end
    
    
    --
    -- print
    --
    
    if data_type == 'ingredient' then
        out[#out+1] = h.head(i18n.acquisition.ingredient_header)
        out[#out+1] = i18n.acquisition.ingredient
    else
        out[#out+1] = h.head(i18n.acquisition.upgraded_from_header)
        out[#out+1] = i18n.acquisition.upgraded_from
    end
    out[#out+1] = '<br>'
    out[#out+1] = tostring(tbl)
end

function h.reward_table(data, rtbl)
    if #data == 0 then
        return
    end
    
    local tbl = m_quest_reward.reward_tbl_head()
            
    for _, row in ipairs(data) do
        local classes
        if row[rtbl .. '.classes'] then
            classes = {}
            for _, class in ipairs(m_util.string.split(row[rtbl .. '.classes'], ',')) do
                classes[class] = true
            end
        end
        
        local tr = tbl:tag('tr')
        m_quest_reward.reward_tbl_row_head(tr, rtbl, row)
                
        local cell = {
            [0] = {
                value = '✗',
                sort = 0,
                class = 'table-cell-xmark',
            },
            [1] = {
                value = '✓',
                sort = 1,
                class = 'table-cell-checkmark',
            },
        }
        if rtbl == 'quest_rewards' then
            local value = m_quest_reward.reward_tbl_extra_info(row)
            
            -- If there isn't any extra information the checkmark will do
            if value ~= '' then
                cell[1].value = value
                cell[1].class = nil
                cell[1].css = 'text-align: center;'
            end
        end
        
        if classes then
            for _, class in ipairs(m_game.constants.characters_order) do
                local cell_value
                if classes[m_game.constants.characters[class].name] then
                    cell_value = cell[1]
                else
                    cell_value = cell[0]
                end
                tr:tag('td')
                    :attr('class', cell_value.class or '')
                    :attr('style', cell_value.css or '')
                    :attr('table-sort-value', cell_value.sort)
                    :wikitext(cell_value.value)
            end
        else
            tr:tag('td')
                :attr('colspan', 7)
                :attr('class', cell[1].class or '')
                :attr('style', cell[1].css or '')
                :attr('table-sort-value', cell[1].sort)
                :wikitext(cell[1].value)
        end
    end
    
    return h.head(i18n.quest_reward[rtbl .. '_header']) .. i18n.quest_reward[rtbl .. '_intro'] .. tostring(tbl)
end

-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------

local p = {}

--
-- Template: Item acquisition
--
function p.item_acquisition (frame)
    --[[
    Duplicates the information from the infobox in a more readable 
    manner on the page.
    
    Example
    -------
    = p.item_acquisition{page='The Rite of Elements'}
    ]]

    -- Get args
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)

    local out = {}
    local results
    local query
    
    out[#out+1] = tpl_args.acquisition_insert
    
    -- fetch general item drop information that is used in multiple places in a
    -- single query to reduce performance hit
    local tables = {'items'}
    local fields = {
        'items._pageName',
        'items.name',
        'items.rarity_id',
        'items.drop_text',
        'items.acquisition_tags',
        'items.drop_areas__full',
        'items.drop_monsters',
        'items.drop_enabled',
        'items.is_drop_restricted',
        'items.drop_level', --added to allow for better information where item can drop based on iLvl
        'items.class_id', --added to prevent issues with div/skill gem cards gaining char-level based acquistion from vendor text
    }
    local query = {
        limit = 1,
    }
    if tpl_args.page then
        -- Join with _pageData in order to check for page redirect
        tables[#tables+1] = '_pageData'
        fields[#fields+1] = '_pageData._pageNameOrRedirect'
        query.where = string.format(
            '_pageData._pageName="%s"',
            tpl_args.page
        )
        query.join = 'items._pageName = _pageData._pageNameOrRedirect'
    else
        query.where = string.format(
            'items._pageName="%s"',
            tostring(mw.title.getCurrentTitle())
        )
    end
    local item_data = m_cargo.query(tables, fields, query)
    if #item_data > 0 then
        item_data = item_data[1]
    end
    
    -- ------------------------------------------------------------------------
    -- General drop restrictions
    -- ------------------------------------------------------------------------
    local drop_enabled = m_util.cast.boolean(item_data['items.drop_enabled'])
    local drop_restricted = m_util.cast.boolean(item_data['items.is_drop_restricted'])
    local is_unique = item_data['items.rarity_id'] == 'unique'
    local is_sold_at_vendor = item_data['items.class_id'] ~= 'DivinationCard'
    local is_skill = item_data['items.class_id'] == 'Active Skill Gem' or 'Support Skill Gem'
    if not drop_enabled then
        out[#out+1] = string.format(i18n.acquisition.drop_disabled, item_data['items.name'])
    elseif drop_restricted then
        out[#out+1] = string.format(i18n.acquisition.drop_restricted, item_data['items.name'])
        if is_unique then
            out[#out+1] = ' '
            out[#out+1] = i18n.acquisition.cannot_be_chanced
        end
    else
        if not item_data['items.drop_areas__full'] then
            out[#out+1] = string.format(i18n.acquisition.drop_anywhere, item_data['items.name'], item_data['items.drop_level'])
        end
        if is_unique then
            out[#out+1] = ' '
            out[#out+1] = i18n.acquisition.can_be_chanced
        elseif is_sold_at_vendor and not is_skill then
	        out[#out+1] = ' '
	        out[#out+1] = string.format(i18n.acquisition.can_be_purchased, item_data['items.name'], item_data['items.drop_level'] - 1)
        end
    end
    
    -- ------------------------------------------------------------------------
    -- League-specific
    -- ------------------------------------------------------------------------
    local acquisition_tags
    if item_data['items.acquisition_tags'] then
        acquisition_tags = m_util.string.split(item_data['items.acquisition_tags'], ',')
    else
        acquisition_tags = {}
    end
    if m_util.table.contains(acquisition_tags, 'league-specific') and drop_enabled and is_unique then
        out[#out+1] = '\n\n'
        out[#out+1] = string.format(i18n.acquisition.league_specific_unique, item_data['items.name'])
    end
    
    -- ------------------------------------------------------------------------
    -- Drop restrictions by text
    -- ------------------------------------------------------------------------
    if item_data['items.drop_text'] and drop_enabled then
        out[#out+1] = '\n\n'
        out[#out+1] = item_data['items.drop_text']
    end
    
    -- ------------------------------------------------------------------------
    -- Drop restrictions by area
    -- ------------------------------------------------------------------------
    
    local area_ids
    if item_data['items.drop_areas__full'] then
        area_ids = m_util.string.split(item_data['items.drop_areas__full'], ',')
    else
        area_ids = {}
    end
    
    -- Handle legacy areas:
    local legacy_area_ids = {
        -- 'MapWar%',
        'MapAtlas%',
        'Map2%',
        'MapTier%',
    }
    local condition_current = {}
    local condition_legacy = {}
    local order_legacy = {}
    for i,v in ipairs(legacy_area_ids) do 
        condition_current[#condition_current+1] = string.format('areas.id NOT LIKE "%s"', v)
        condition_legacy[#condition_legacy+1] = string.format('areas.id LIKE "%s"', v)
        order_legacy[#order_legacy+1] = string.format('WHEN areas.id LIKE "%s" THEN %d', v,i)        
    end
    
    local query_sets = {
        {
            header=i18n.acquisition.area_header,
            condition=string.format('%s', table.concat(condition_current, ' AND ')),
            order='areas.name ASC',
        },
        {
            header=i18n.acquisition.area_legacy_header,
            condition=string.format('%s', table.concat(condition_legacy, ' OR ')),
            order=string.format([[
                CASE 
                    %s
                    ELSE %d 
                END ASC, 
                areas.name ASC
            ]], 
            table.concat(order_legacy, ' '), 
            #order_legacy+1
            ),
        },
    }
        
    if #area_ids > 0 then
        for _, query_set in ipairs(query_sets) do
            local results = m_cargo.query(
                {'areas'},
                {
                    'areas._pageName',
                    'areas.id',
                    'areas.name',
                    'areas.main_page',
                },
                {
                    where=string.format('(%s) AND areas.id IN ("%s")', query_set.condition, table.concat(area_ids, '","')),
                    orderBy=query_set.order,
                    groupBy='areas.id',
                }
            )
            if #results > 0 then
                local ul = mw.html.create('ul')
                for _, row in ipairs(results) do
                    ul:tag('li')
                        :wikitext(string.format('[[%s|%s]]', row['areas.main_page'] or row['areas._pageName'], row['areas.name']))
                end
                out[#out+1] = h.head(query_set.header)
                out[#out+1] = i18n.acquisition.area
                out[#out+1]= '<br>'
                out[#out+1] = tostring(ul)
            end
        end
    end
    
    -- ------------------------------------------------------------------------
    -- Drop restrictions by monster
    -- ------------------------------------------------------------------------

    local monster_metadata_ids = {}
    if item_data['items.drop_monsters'] then
        monster_metadata_ids = m_util.string.split(item_data['items.drop_monsters'], ',%s*')
    end
            
    if #monster_metadata_ids > 0 then
        local results = m_cargo.query(
            {'monsters', 'main_pages'},
            {
                'monsters._pageName',
                'monsters.metadata_id',
                'monsters.name',
                -- 'monsters.main_page',
                'main_pages._pageName',
            },
            {
                join='monsters.metadata_id=main_pages.id',
                where=string.format('monsters.metadata_id IN ("%s")', table.concat(monster_metadata_ids, '","')),
                orderBy='monsters.name',
                groupBy='monsters.metadata_id',
            }
        )
        if #results > 0 then
            local ul = mw.html.create('ul')
            for _, row in ipairs(results) do
                ul:tag('li')
                    :wikitext(string.format(
                        '[[%s|%s]]', 
                           row['monsters.main_page'] 
                        or row['main_pages._pageName'] 
                        or row['monsters._pageName'], 
                        row['monsters.name']
                    ))
            end
            out[#out+1] = h.head(i18n.acquisition.monster_header)
            out[#out+1] = i18n.acquisition.monster
            out[#out+1]= '<br>'
            out[#out+1] = tostring(ul)
        end
    end
    
    -- ------------------------------------------------------------------------
    -- Vendor recipes/upgrades handling
    -- ------------------------------------------------------------------------
    
    --
    -- Query set data
    --
    local obtained_sets, obtained_sets_order = h.fetch_upgraded_from_sets(
        string.format('upgraded_from_sets._pageName="%s"', item_data['items._pageName']),
        string.format('upgraded_from_groups._pageName="%s"', item_data['items._pageName'])
    )
    
    results = m_cargo.query(
        {'upgraded_from_groups'},
        {
            'upgraded_from_groups._pageID',
            'upgraded_from_groups.set_id',
        },
        {
            where=string.format('upgraded_from_groups.item_page="%s"', item_data['items._pageName']),
            -- only need one result set for the where clause
            groupBy='upgraded_from_groups._pageID, upgraded_from_groups.set_id',
        }
    )
    local where = {sets={}, groups={}}
    for key, data in pairs(where) do
        for _, row in ipairs(results) do
            data[#data+1] = string.format('upgraded_from_%s.set_id="%s" AND upgraded_from_%s._pageID="%s"', key, row['upgraded_from_groups.set_id'], key, row['upgraded_from_groups._pageID'])
        end
        where[key] = table.concat(data, ' OR ')
    end
    local ingredient_sets, ingredient_sets_order = h.fetch_upgraded_from_sets(where.sets, where.groups)
    --
    -- Query bulk item info for item linking
    --
    local item_pages = {assoc={}}
    for _, set_data in ipairs({{obtained_sets, obtained_sets_order}, {ingredient_sets, ingredient_sets_order}}) do
        for _, set_key in ipairs(set_data[2]) do
            local set = set_data[1][set_key]
            for _, group in ipairs(set.groups) do
                item_pages.assoc[group['upgraded_from_groups.item_page']] = true
            end
            item_pages.assoc[set['upgraded_from_sets._pageName']] = true
        end
    end
    -- remove duplicates
    for name, _ in pairs(item_pages.assoc) do
        item_pages[#item_pages+1] = name
    end
    
    if #item_pages < cfg.MAX_ITEMS then
        item_pages = m_cargo.map_results_to_id{
            results=m_cargo.array_query{
                tables={'items'},
                fields={'items.name', 'items.inventory_icon', 'items.html'},
                id_array = item_pages,
                id_field = 'items._pageName',
                query = {
                    limit=5000
                },
            },
            field='items._pageName',
            keep_id_field=true,
        }
    end
    
    -- 
    -- Output for being obtained via vendor recipe
    --
    
    h.upgraded_from_table(tpl_args, obtained_sets, obtained_sets_order, 'obtained', out, item_pages)
    
    -- 
    -- Ingredient of vendor recipes/upgrades
    -- 
    
    h.upgraded_from_table(tpl_args, ingredient_sets, ingredient_sets_order, 'ingredient', out, item_pages)
    
    out[#out+1] = tpl_args.ingredient_append
    
    -- ------------------------------------------------------------------------
    -- Obtained via quest or vendor reward
    -- ------------------------------------------------------------------------
    
    local quest_rewards = m_cargo.query(
        {'quest_rewards'},
        {
            'quest_rewards.quest',
            'quest_rewards.act',
            'quest_rewards.classes',
            'quest_rewards.sockets',
            'quest_rewards.item_level',
            'quest_rewards.rarity',
        },
        {
            where=string.format('quest_rewards._pageName="%s"', item_data['items._pageName']),
            orderBy='quest_rewards.act ASC, quest_rewards.quest_id ASC',
        }
    )
    out[#out+1] = h.reward_table(quest_rewards, 'quest_rewards')
    
    local vendor_rewards = m_cargo.query(
        {'vendor_rewards'},
        {
            'vendor_rewards.quest',
            'vendor_rewards.act',
            'vendor_rewards.classes',
            'vendor_rewards.npc',
        },
        {
            where=string.format('vendor_rewards._pageName="%s"', item_data['items._pageName']),
            orderBy='vendor_rewards.act ASC, vendor_rewards.quest_id ASC',
        }
    )
    out[#out+1] = h.reward_table(vendor_rewards, 'vendor_rewards')
    
    -- ------------------------------------
    -- output
    -- ------------------------------------
    
    local head = mw.html.create('h2')
    head:wikitext(i18n.acquisition.header .. i18n.acquisition.link)
    return tostring(head) .. table.concat(out)
end

return p