Module:Sandbox/Illviljan/Area

From Path of Exile Wiki
Jump to navigation Jump to search
Module documentation[create] [purge]
-- SMW powered area module

-- ----------------------------------------------------------------------------
-- TODO
-- ----------------------------------------------------------------------------
-- invalid tags -> utl?
-- spawn_weight* values
-- spawnchacne values
-- 
-- i18n for properties

-- ----------------------------------------------------------------------------
-- Imports
-- ----------------------------------------------------------------------------

local m_util = require('Module:Util')
local getArgs = require('Module:Arguments').getArgs
local m_game = require('Module:Game')
local f_infocard = require('Module:Infocard')._main

local cargo = mw.ext.cargo

-- ----------------------------------------------------------------------------
-- Localization
-- ----------------------------------------------------------------------------

-- Strings
local i18n = {
    images = {
        waypoint_no = '[[File:No waypoint area icon.png|link=|No Waypoint]]',
        waypoint_yes = '[[File:Waypoint area icon.png|link=|No Waypoint]]',
        waypoint_town = '[[File:Town area icon.png|link=|Town Hub]]',
        loading_screen = 'File:%s loading screen.png',
        loading_screen_infobox = '[[File:%s loading screen.png|250px]]',
        screenshot = 'File:%s area screenshot.%s',
        screenshot_infobox = '[[File:%s area screenshot.%s|250px]]',
    },

    args = {
        main_page = 'main_page',
        id = 'id',
        name = 'name',
        act = 'act',
        area_level = 'level',
        level_restriction_max = 'level_restriction_max',
        area_type_tags = 'area_type_tags',
        tags = 'tags',
        loading_screen = 'loading_screen',
        connection_ids = 'connection_ids',
        parent_area_id = 'parent_area_id',
        modifier_ids = 'modifier_ids',
        boss_monster_ids = 'boss_monster_ids',
        monster_ids = 'monster_ids',
        entry_text = 'entry_text',
        entry_npc = 'entry_npc',
        flavour_text = 'flavour_text',
        screenshot_ext = 'screenshot_ext',
        vaal_area_spawn_chance = 'vaal_area_spawn_chance',
        vaal_area_ids = 'vaal_area_ids',
        strongbox_spawn_chance = 'strongbox_spawn_chance',
        strongbox_max_count = 'strongbox_max',
        strongbox_rarity_weight = 'strongbox_rarity_weight',
        is_map_area = 'is_map_area',
        is_unique_map_area = 'is_unique_map_area',
        is_town_area = 'is_town_area',
        is_hideout_area = 'is_hideout_area',
        is_vaal_area = 'is_vaal_area',
        is_master_daily_area = 'is_master_daily_area',
        is_labyrinth_area = 'is_labyrinth_area',
        is_labyrinth_airlock_area = 'is_labyrinth_airlock_area',
        is_labyrinth_boss_area = 'is_labyrinth_boss_area',
        has_waypoint = 'has_waypoint',
    },
    errors = {
        invalid_tag = '%s is not a valid tag',
        main_page_is_invalid = 'main_page argument got "%s" which is not a valid wiki page', 
        main_page_does_not_exist = 'main_page argument requires the specified page "%s" to exist in the main wiki namespace',
    },
    tooltips = {
        -- boolean tooltips. Use singular form here, categories makes these plural.
        is_map_area = 'Map area',
        is_unique_map_area = 'Unique Map area',
        is_town_area = 'Town area',
        is_hideout_area = 'Hideout area',
        is_vaal_area = 'Vaal area',
        is_master_daily_area = 'Master daily spawn area',
        is_labyrinth_area = 'Labyrinth area',
        is_labyrinth_airlock_area = 'Labyrinth airlock area',
        is_labyrinth_boss_area = 'Labyrinth boss area',
        area = 'area',
        
        --
        entry_message = '%s: %s',
    },
    headers = {
        id = 'Id',
        act = 'Act',
        area_level = 'Area level',
        level_restriction_max = m_util.html.abbr('Max level', 'Characters above this level can not enter this zone.'),
        area_type_tags = 'Area type tags',
        tags = 'Tags',
        parent_area = m_util.html.abbr('Parent Town', 'Portals will lead to the parent town'),
        connections = 'Connections',
        -- monsters
        -- boss monsters
        vaal_areas = 'Vaal Areas',
    },
}

function m_util.args.spawn_weight_list (argtbl, args)
    -- Parses a weighted pair of lists and sets properties
    --
    -- argtbl: argument table to work with
    -- args:
    --  output_argument - if set, set arguments to this value
    --  frame - if set, automtically set subobjects
    --  input_argument - input prefix for parsing the arguments from the argtbl
    --  weight_property - name of the weight property on the subobject
    --  subobject_name - name of the subobject 
    args = args or {}
    args.input_argument = 'spawn_weight'
    args.output_argument = 'spawn_weights'
    args.weight_property = 'Has spawn weight'
    args.subobject_name = 'spawn weight'
    args.cargo_table = 'spawn_weights'

    local i = 0
    local id = nil
    local value = nil
    
    if args.output_argument then
        argtbl[args.output_argument] = {}
    end

    repeat
        i = i + 1
        id = {
            tag = string.format('%s%s_tag', args.input_argument, i),
            value = string.format('%s%s_value', args.input_argument, i),
        }
    
        value = {
            tag = argtbl[id.tag],
            value = argtbl[id.value],
        }
        
        if value.tag ~= nil and value.value ~= nil then
            if args.output_argument then
                argtbl[args.output_argument][i] = value
            end
            
            if args.frame then
                if args.cargo_table then
                    m_util.cargo.store(args.frame, {
                        _table = args.cargo_table,
                        ordinal = i,
                        tag = value.tag,
                        weight = m_util.cast.number(value.value, {min=0}),
                    })
                end
            end
        elseif not (value.tag == nil and value.value == nil) then
            error(string.format(i18n.errors.invalid_weight, id.tag, id.value))
        end
    until value.tag == nil
end


-- ----------------------------------------------------------------------------
-- Constats
-- ----------------------------------------------------------------------------

local c = {}
-- TODO: test
c.max_area_query = 8

-- ----------------------------------------------------------------------------
-- Utility & helper functions
-- ----------------------------------------------------------------------------

local factory = {}
function factory.arg_list(k, args)
    return function (tpl_args, frame)
        if tpl_args[k] ~= nil then
            tpl_args[k] = m_util.string.split(tpl_args[k], ', ')
        end
    end
end

function factory.display_value(k, args)
    return function(tpl_args, frame)
        return tpl_args[k]
    end
end

local util = {}
util.display = {}
function util.display.multiple_areas(tpl_args, frame, area_ids)
    local out = {}
    for _, area_id in ipairs(area_ids) do
        out[#out+1] = util.display.single_area(tpl_args, frame, area_id)
    end
    return table.concat(out, '<br>')
end

function util.display.single_area(tpl_args, frame, area_id)
    if tpl_args.areas[area_id] then
        if tpl_args.areas[area_id]['areas.main_page'] ~= '' then
            return string.format('[[%s|%s]]', tpl_args.areas[area_id]['areas.main_page'], tpl_args.areas[area_id]['areas.name'])
        else
            return string.format('%s ([[%s|%s]])', tpl_args.areas[area_id]['areas.name'], tpl_args.areas[area_id]['areas._pageName'], area_id)
        end
    else
        return area_id
    end
end

-- ----------------------------------------------------------------------------
-- Argument & display mapping
-- ----------------------------------------------------------------------------

local argument_map = {
    table = 'areas',
    --
    -- User supplied arguments
    -- 
    fields = {
        main_page = {
            field = 'main_page', 
            type = 'Page',
            func = function(tpl_args, frame)
                local page = tpl_args.main_page 
                if page ~= nil then
                    page = mw.title.new(tpl_args.main_page)
                    if page == nil then
                        error(string.format(i18n.errors.main_page_is_invalid, tpl_args.main_page))
                    elseif not page.exists then
                        error(string.format(i18n.errors.main_page_does_not_exist, tpl_args.main_page))
                    else
                        -- dont need the title object anymore
                        --tpl_args.main_page = page
                    end
                end
            end
        },
        
        --
        -- Can be populated by PyPoE
        --
        id = {
            field = 'id',
            type = 'String',
            func = nil,
        },
        name = {
            field = 'name',
            type = 'String',
            func = nil,
        },
        act = {
            field = 'act',
            type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.act, {key_out='act'}),
        },
        area_level = {
            field = 'area_level',
            type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.area_level, {key_out='area_level'}),
        },
        level_restriction_max = {
            field = 'level_restriction_max',
            type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.level_restriction_max, {key_out='level_restriction_max'}),
            default = 100,
        },
        area_type_tags = {
            field = 'area_type_tags',
            type = 'List (,) of String',
            func = m_util.cast.factory.assoc_table(i18n.args.area_type_tags, {
                tbl = m_game.constants.tags,
                errmsg = i18n.errors.invalid_tag,
                key_out = 'area_type_tags',
            }),
        },
        tags = {
            field = 'tags',
            type = 'List (,) of String',
            func = m_util.cast.factory.assoc_table(i18n.args.tags, {
                tbl = m_game.constants.tags,
                errmsg = i18n.errors.invalid_tag,
                key_out = 'tags',
            }),
        },
        loading_screen = {
            field = 'loading_screen',
            type = 'Page',
            func = function (tpl_args, frame)
                local loading_id = tpl_args[i18n.args.loading_screen]
                if loading_id ~= nil then
                    tpl_args.loading_screen = string.format(i18n.images.loading_screen, loading_id)
                    tpl_args.loading_screen_infobox = string.format(i18n.images.loading_screen_infobox, loading_id) 
                end
            end,
        },
        connection_ids = {
            field = 'connection_ids',
            type = 'List (,) of String',
            func = factory.arg_list(i18n.args.connection_ids, {key_out='connection_ids'}),
            default = {},
        },
        parent_area_id = {
            field = 'parent_area_id',
            type = 'String',
        },
        modifier_ids = {
            field = 'modifier_ids',
            type = 'List (,) of String',
            func = function(tpl_args, frame)
                factory.arg_list(i18n.args.modifier_ids, {key_out='modifier_ids'})(tpl_args, frame)
                
                if tpl_args.modifier_ids == nil then
                    return
                end
                
                local query_ids = {}
                for i, mod_id in ipairs(tpl_args.modifier_ids) do
                    query_ids[#query_ids+1] = string.format('mods.id="%s"', mod_id)
                end
                
                local results = cargo.query(
                    'mods',
                    'mods.id, mods.stat_text',
                    {
                        where=table.concat(query_ids, ' OR '),
                    }
                )
                
                local text = {}
                for page, row in pairs(results) do
                    if row['mods.stat_text'] ~= '' then
                        text[#text+1] = row['mods.stat_text']
                    end
                end
                
                tpl_args.stat_text = table.concat(text, '<br>')
            end,
            default = {},
        },
        monster_ids = {
            field = 'monster_ids',
            type = 'List (,) of String',
            func = factory.arg_list(i18n.args.monster_ids, {key_out='monster_ids'}),
            default = {},
        },
        boss_monster_ids = {
            field = 'boss_monster_ids',
            type = 'List (,) of String',
            func = factory.arg_list(i18n.args.boss_monster_ids, {key_out='boss_monster_ids'}),
            default = {},
        },
        entry_text = {
            field = 'entry_text',
            type = 'Text',
        },
        entry_npc = {
            field = 'entry_npc',
            type = 'String',
        },
        flavour_text = {
            field = 'flavour_text',
            type = 'Text',
        },
        screenshot_ext = {
            func = nil,
            default = 'jpg',
        },
        screenshot = {
            field = 'screenshot',
            type = 'Page',
            func = function(tpl_args, frame)
                if tpl_args.name ~= nil then
                    local name = tpl_args.screenshot or tpl_args.main_page or tpl_args.name
                    tpl_args.screenshot = string.format(i18n.images.screenshot, name, tpl_args.screenshot_ext)
                    tpl_args.screenshot_infobox = string.format(i18n.images.screenshot_infobox, name, tpl_args.screenshot_ext)
                end
            end,
        },
        --
        -- Spawn chances
        --
        vaal_area_ids = {
            field = 'vaal_area_ids',
            type = 'List (,) of String',
            func = factory.arg_list(i18n.args.vaal_area_ids, {key_out='vaal_area_ids'}),
            default = {},
        },
        vaal_area_spawn_chance = {
            field = 'vaal_area_spawn_chance',
            type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.vaal_area_spawn_chance, {key_out='vaal_area_spawn_chance'}),
            default = 0,
        },
        strongbox_spawn_chance = {
            field = 'strongbox_spawn_chance',
            type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.strongbox_spawn_chance, {key_out='strongbox_spawn_chance'}),
            default = 0,
        },
        strongbox_max_count = {
            field = 'strongbox_max_count',
            type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.strongbox_max_count, {key_out='strongbox_max_count'}),
            default = 0,
        },
        -- fields are handled for this below
        strongbox_rarity_weight = {
            func = function (tpl_args, frame)
                local weights = m_util.string.split(tpl_args[i18n.args.strongbox_rarity_weight] or '', ', ')
                
                tpl_args.strongbox_rarity_weight = {}
                
                for index, data in ipairs(m_game.constants.item.rarity) do
                    local value = tonumber(weights[index]) or 0
                    tpl_args.strongbox_rarity_weight[data.long_lower] = value
                    tpl_args._properties[string.format('strongbox_weight_%s', data.long_lower)] = value 
                end
            end,
        },
        --
        -- Area flags
        --
        is_map_area = {
            field = 'is_map_area',
            type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_map_area, {key_out='is_map_area'}),
            default = false,
        },
        is_unique_map_area = {
            field = 'is_unique_map_area',
            type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_unique_map_area, {key_out='is_unique_map_area'}),
            default = false,
        },
        is_town_area = {
            field = 'is_town_area',
            type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_town_area, {key_out='is_town_area'}),
            default = false,
        },
        is_hideout_area = {
            field = 'is_hideout_area',
            type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_hideout_area, {key_out='is_hideout_area'}),
            default = false,
        },
        is_vaal_area = {
            field = 'is_vaal_area',
            type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_vaal_area, {key_out='is_vaal_area'}),
            default = false,
        },
        is_master_daily_area = {
            field = 'is_is_master_daily_area',
            type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_master_daily_area, {key_out='is_master_daily_area'}),
            default = false,
        },
        is_labyrinth_area = {
            field = 'is_labyrinth_area',
            type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_labyrinth_area, {key_out='is_labyrinth_area'}),
            default = false,
        },
        is_labyrinth_airlock_area = {
            field = 'is_labyrinth_airlock_area',
            type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_labyrinth_airlock_area, {key_out='is_labyrinth_airlock_area'}),
            default = false,
        },
        is_labyrinth_boss_area = {
            field = 'is_labyrinth_boss_area',
            type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_labyrinth_boss_area, {key_out='is_labyrinth_boss_area'}),
            default = false,
        },
        has_waypoint = {
            field = 'has_waypoint',
            type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.has_waypoint, {key_out='has_waypoint'}),
            default = false,
        },
        --
        -- Handled elsewhere in the old
        --
        release_version = {
            field = 'release_version',
            type = 'String'
        },
        removal_version = {
            field = 'removal_version',
            type = 'String',
        },
        mainpage_categories = {
            field = 'mainpage_categories',
            type = 'List (,) of String',
        },
        infobox_html = {
            field = 'infobox_html',
            type = 'Text',
        },
        strongbox_weight_normal = {
            field = 'strongbox_weight_normal',
            type = 'Integer',
        },
        strongbox_weight_magic = {
            field = 'strongbox_weight_magic',
            type = 'Integer',
        },
        strongbox_weight_rare = {
            field = 'strongbox_weight_rare',
            type = 'Integer',
        },
        strongbox_weight_unique = {
            field = 'strongbox_weight_unique',
            type = 'Integer',
        },
        stat_text = {
            field = 'stat_text',
            type = 'Text',
        },
        --
        -- Not argument to the template, but still parsing arguments
        --
        areas = {
            func = function(tpl_args, frame)
                local query_ids = {}
                for _, arg in ipairs({'parent_area_id', 'connection_ids', 'vaal_area_ids'}) do 
                    if type(tpl_args[arg]) == 'table' then
                        for _, tbl_id in ipairs(tpl_args[arg]) do
                            query_ids[tbl_id] = {}
                        end
                    elseif tpl_args[arg] then
                        query_ids[tpl_args[arg]] = {}
                    end
                end
                
                local query_ids_trimmed = {}
                for k, _ in pairs(query_ids) do
                    if type(k) ~= 'number' then
                        query_ids_trimmed[#query_ids_trimmed+1] = string.format('areas.id="%s"', k)
                    end
                end
                
                if #query_ids_trimmed == 0 then
                    tpl_args.areas = {}
                else
                    local result = cargo.query(
                        'areas',
                        'areas._pageName, areas.id, areas.name, areas.main_page',
                        {
                            where=table.concat(query_ids_trimmed, ' OR '),
                        }
                    )
                    
                    tpl_args.areas = {}
                    for _, row in ipairs(result) do
                        tpl_args.areas[row['areas.id']] = row
                    end
                end
                -- TODO: Error/Warning for missing areas?
            end,
        },
    },
}

local argument_order = {
    'main_page',
    'id',
    'name',
    'act',
    'area_level',
    'level_restriction_max',
    'area_type_tags',
    'tags',
    'loading_screen',
    'connection_ids',
    'parent_area_id',
    'modifier_ids',
    'boss_monster_ids',
    'monster_ids',
    'entry_text',
    'entry_npc',
    'flavour_text',
    'screenshot_ext',
    'screenshot',
    'vaal_area_spawn_chance',
    'vaal_area_ids',
    'strongbox_spawn_chance',
    'strongbox_max_count',
    'strongbox_rarity_weight',
    'is_map_area',
    'is_unique_map_area',
    'is_town_area',
    'is_hideout_area',
    'is_vaal_area',
    'is_master_daily_area',
    'is_labyrinth_area',
    'is_labyrinth_airlock_area',
    'is_labyrinth_boss_area',
    'has_waypoint',
    -- parsed by m_util.args.version:
    -- 'release_version',
    -- 'removal_version',
    
    -- Non argument, but passed to the script
    'areas',
}

local display  = {}
display.area_type = {
    'is_map_area',
    'is_unique_map_area',
    'is_town_area',
    'is_hideout_area',
    'is_vaal_area',
    'is_master_daily_area',
    'is_labyrinth_area',
    'is_labyrinth_airlock_area',
    'is_labyrinth_boss_area',
}

display.table_map = {
    {
        args = {
            id = {
            },
        },
        header = i18n.headers.id,
        func = function(tpl_args, frame)
            return string.format('[[%s|%s]]', mw.title.getCurrentTitle().fullText, tpl_args.id)
        end,
    },
    {
        args = {
            act = {
            },
        },
        header = i18n.headers.act,
        func = factory.display_value('act'),
    }, 
    {
        args = {
            area_level = {
            },
        },
        header = i18n.headers.area_level,
        func = factory.display_value('area_level'),
    },
    {
        args = {
            level_restriction_max = {
                hide = 100,
            },
        },
        header = i18n.headers.level_restriction_max,
        func = factory.display_value('level_restriction_max'),
    },
    {
        args = {
            area_type_tags = {
            },
        },
        header = i18n.headers.area_type_tags,
        func = function(tpl_args, frame)
            return table.concat(tpl_args.area_type_tags, ', ')
        end,
    },
    {
        args = {
            tags = {
            },
        },
        header = i18n.headers.tags,
        func = function(tpl_args, frame)
            return table.concat(tpl_args.tags, ', ')
        end,
    },
    {
        args = {
            entry_text = {},
            entry_npc = {},
        },
        header = i18n.headers.entry_messsage,
        func = function(tpl_args, frame)
            return m_util.html.poe_color('quest',  -- Any other alternatives for spoken text?
                string.format(i18n.tooltips.entry_message, tpl_args.entry_npc, tpl_args.entry_text) 
            )
        end,
    },
    {
        args = {
            parent_area_ids = {},
        },
        header = i18n.headers.parent_area,
        func = function(tpl_args, frame)
            return util.display.single_area(tpl_args, frame, tpl_args.parent_area)
        end,
    },
    {
        args = {
            connection_ids = {},
        },
        header = i18n.headers.connections,
        func = function(tpl_args, frame)
            return util.display.multiple_areas(tpl_args, frame, tpl_args.connection_ids)
        end,
    },
    {
        args = {
            vaal_area_ids = {},
        },
        header = i18n.headers.vaal_areas,
        func = function(tpl_args, frame)
            return util.display.multiple_areas(tpl_args, frame, tpl_args.vaal_area_ids)
        end,
    },
}

display.list_map = {
    {
        args = {
            flavour_text = {},
        },
        func = function(tpl_args, frame)
            return m_util.html.poe_color('flavour', tpl_args.flavour_text)
        end,
    },
    {
        args = {
            loading_screen_infobox = {},
        },
        func = factory.display_value('loading_screen_infobox'),
    },
    {
        args = {
            screenshot_infobox = {},
        },
        func = factory.display_value('screenshot_infobox'),
    },
    {
        args = {
            stat_text = {},
        },
        func = function(tpl_args, frame)
            return m_util.html.poe_color('mod', tpl_args.stat_text)
        end,
    },
}

-- ----------------------------------------------------------------------------
-- display functions
-- ----------------------------------------------------------------------------

local d = {}

function d.intro_text(tpl_args, frame)
    --[[
    Display an introductory text about the area.
    ]]
    local out = {}
    if mw.ustring.find(tpl_args['id'], '_') then
        out[#out+1] = frame:expandTemplate{
            title='Incorrect title', 
            args = {title=tpl_args['id']} 
        }
    end
    
    if tpl_args['name'] then
        out[#out+1] = string.format(
            "'''%s''' is the internal id for the [[%s|%s]] area. ", 
            tpl_args['id'], 
            tpl_args['main_page'] or tostring(mw.title.getCurrentTitle()), 
            tpl_args['name']
        )
    else 
        out[#out+1] = string.format(
            "'''%s''' is the internal id of an unnamed area. ",
            tpl_args['id']
        )
    end 

    local connected_areas = {}
    for _, arg in ipairs({'connection_ids', 'vaal_area_ids'}) do  
        if tpl_args[arg] then
            for _, id in ipairs(tpl_args[arg]) do
                if tpl_args.areas[id] then 
                    connected_areas[#connected_areas+1] = string.format(
                        '<li>[[%s|%s]] (%s)</li>', 
                        tostring(tpl_args.areas[id]['areas._pageName']), 
                        id, 
                        tostring(tpl_args.areas[id]['areas.name'])
                    )
                end
            end
        end
    end
    if #connected_areas > 0 then 
        out[#out+1] = string.format(
            'It is connected to the following areas:<ul>%s</ul>', 
            table.concat(connected_areas)
        )
    end
    
    return table.concat(out)
end

function d._check_args(tpl_args, frame, data)
    local continue = true
    if data.args ~= nil then
        for key, key_data in pairs(data.args) do
            if tpl_args[key] == nil or (type(tpl_args[key]) == 'table' and #tpl_args[key] == 0) then
                continue = false
                break
            elseif type(key_data.hide) == 'table' then
                local br = false
                for _, value in ipairs(key_data.hide) do
                    if tpl_args[key] == value then
                        br = true
                        break
                    end
                end
                if br then
                    continue = false
                    break
                end
            elseif key_data.hide ~= nil and tpl_args[key] == key_data.hide then
                continue = false
                break
            end
        end
    end
    return continue
end

function d.area_box(tpl_args, frame)
    --[[
    Display the area info box.
    ]]
    
    local infocard_args = {}
    
    infocard_args.header = tpl_args.name
    
    -- Subheader
    local out = {}
    for _, key in ipairs(display.area_type) do
        if tpl_args[key] == true then
            out[#out+1] = i18n.tooltips[key]
        end
    end

    if #out > 0 then
        infocard_args.subheader = table.concat(out, ', ')
    else
        infocard_args.subheader = i18n.tooltips.area
    end
    
    -- Side header
    if tpl_args.is_town_area then
        infocard_args.headerright = i18n.images.waypoint_town
    elseif tpl_args.has_waypoint then
        infocard_args.headerright = i18n.images.waypoint_yes
    else
        infocard_args.headerright = i18n.images.waypoint_no
    end
    
    -- Main sections, loop through
    local tbl = mw.html.create('table')
    for _, data in ipairs(display.table_map) do
        if d._check_args(tpl_args, frame, data) then
            tbl
                :tag('tr')
                    :tag('th')
                        :wikitext(data.header or '')
                        :done()
                    :tag('td')
                        :wikitext(data.func(tpl_args, frame) or '')
                        :done()
                    :done()
        end
    end
    
    infocard_args[1] = tostring(tbl)
    
    local i = 2
    for _, data in ipairs(display.list_map) do
        if d._check_args(tpl_args, frame, data) then
            infocard_args[i] = data.func(tpl_args, frame)
            i = i + 1
        end
    end

    return f_infocard(infocard_args)
end

function d.subobject_box(tpl_args, frame)
    --[[
    Display the subobject box.
    ]]
    local container = mw.html.create('div')
    container
        :attr('class', 'modbox floatright')
        
    -- spawn weight table 
    tbl = container:tag('table')
    tbl
        :attr('class', 'wikitable sortable')
        -- :attr('style', 'style="width: 100%;"')
        :tag('tr')
            :tag('th')
                :attr('colspan', 3)
                :wikitext('Spawn Weights')
                :done()
            :done()
        :tag('tr')
            :tag('th')
                :wikitext('#')
                :done()
            :tag('th')
                :wikitext('Tag')
                :done()
            :tag('th')
                :wikitext('Has spawn weight')
                :done()
            :done()
        :done()
        
    local i = 0
    local value = nil
    repeat
        i = i + 1
        value = {
            tag = tpl_args[string.format('spawn_weight%s_tag', i)],
            value = tpl_args[string.format('spawn_weight%s_value', i)],
        }
        
        if value.tag then
            tbl
                :tag('tr')
                    :tag('td')
                        :wikitext(i)
                        :done()
                    :tag('td')
                        :wikitext(value.tag)
                        :done()
                    :tag('td')
                        :wikitext(value.value)
                        :done()
                    :done()
                :done()
        end
    until value.tag == nil
    
    return tostring(container)
end

-- ----------------------------------------------------------------------------
-- Page functions
-- ----------------------------------------------------------------------------

local p = {}

p.table_areas = m_util.cargo.declare_factory{data=argument_map}

function p.area(frame)
    --[[
    This function adds cargo tables and displays information about the 
    area.
    
    Examples
    --------
    = p.area{
        id = '1_1_1', 
        name = 'The Twilight Strand', 
        act = 1, 
        level = 1, 
        tags = 'no_tempests, area_with_water', 
        loading_screen = 'Act1', 
        connection_ids = '1_1_town',
        parent_area_id = '1_1_town',  
        boss_monster_ids = 'Metadata/Monsters/ZombieBoss/ZombieBossHillockNormal', 
        flavour_text = 'Hope was drowned here.', 
        main_page = 'The Twilight Strand (Act 1)'
    }  
    = p.area{
        id = '1_1_1', 
        name = 'The Twilight Strand', 
        act = 1, 
        level = 1, 
        tags = 'no_tempests, area_with_water', 
        loading_screen = 'Act1', 
        connection_ids = '1_1_town',
        parent_area_id = '1_1_town',  
        boss_monster_ids = 'Metadata/Monsters/ZombieBoss/ZombieBossHillockNormal', 
        flavour_text = 'Hope was drowned here.', main_page = 'The Twilight Strand (Act 1)'
    }
    = p.area{
        id = '1_4_2', 
        name = 'The Dried Lake', 
        act = '4', 
        level = '34', 
        area_type_tags = 'shore', 
        tags = 'act_boss_area', 
        loading_screen = 'Act4', 
        connection_ids = '1_4_town', 
        parent_area_id = '1_4_town', 
        boss_monster_ids = 'Metadata/Monsters/Voll/VollBoss, Metadata/Monsters/SkeletonSoldier/SkeletonSoldierRangedBoss', 
        vaal_area_spawn_chance = '18', 
        vaal_area_ids = '1_SideArea4_2, 1_SideArea4_4', 
        strongbox_spawn_chance = '30', 
        strongbox_max = '2', strongbox_rarity_weight = '50, 50, 50, 1', 
        flavour_text = 'Bones of betrayal, ashes of purity.', main_page = 'The Dried Lake'}
    ]]
    
    
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
    
    --
    -- Shared args
    --

    tpl_args._properties = {
        _table = argument_map.table,
    }
    
    -- Handle release_version and removal_version
    m_util.args.version(tpl_args, {frame=frame})
    
    -- Parse args
    for _, k in ipairs(argument_order) do
        local data = argument_map.fields[k]
        if data == nil then
            error('Missing data in argument_map: ' .. k)
        end
        if data.func ~= nil then
            data.func(tpl_args, frame)
        end
        
        if data.default ~= nil and tpl_args[k] == nil then
            tpl_args[k] = data.default
        end
    end
    
    -- This should include all values as long they're set, regardless 
    -- of whether they are parsed above 
    for key, data in pairs(argument_map.fields) do
        local v = tpl_args[key]
        if data.field ~= nil and v ~= nil then
            tpl_args._properties[data.field] = v
        end
    end
    
    -- Parse spawn weights
    m_util.args.spawn_weight_list(tpl_args, {
        frame=frame, 
    })
    
    -- Category handling for main page only by adding the categories in a property:
    -- Do notice the plural form.
    -- -- Areas, Act X areas/Map areas, Unique map areas 
    local cats = {
        'Category:Areas', 
    }
    local cats_ini_len = #cats
    for _, key in ipairs(display.area_type) do
        if tpl_args[key] == true then
            cats[#cats+1] = string.format('Category:%ss', i18n.tooltips[key])
        end
    end
    if #cats == cats_ini_len then 
        cats[#cats+1] = string.format('Category:Act %s areas', tpl_args.act)
    end

    tpl_args._properties['mainpage_categories'] = table.concat(cats, ',')
    
    -- Category handling for the local data page:
    local page_cats = {
        'Area data',
    }
    
    -- Display only on main pages:
    local out = {}
    out[#out+1] = d.area_box(tpl_args, frame)
    
    -- Property to store what's output to main pages:
    tpl_args._properties['infobox_html'] = out[1]
    
    -- Set all semantic properties:
    mw.logObject('Cargo:' .. m_util.cargo.store(frame, tpl_args._properties))
    mw.logObject(tpl_args._properties)
    
    -- Display only on data page:
    out[#out+1] = d.subobject_box(tpl_args, frame)
    out[#out+1] = d.intro_text(tpl_args, frame)
    
    -- Output of function
    return table.concat(out) .. m_util.misc.add_category(page_cats)
end

function p.query_area_info(frame)
    --[[
    Queries and displays the area infobox. 
    
    ]]
    -- = p.query_area_info{conditions = '[[Is area id::test]]', cats='yes'}
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
    
    if tpl_args.where == nil then
        return
    end
    
    tpl_args.cats = m_util.cast.boolean(tpl_args.cats)
    
    local results = cargo.query(
        'areas',
        'areas.infobox_html, areas.mainpage_categories',
        {
            where=tpl_args.where,
            orderBy=tpl_args.order_by,
            -- Temporary fix for cargo duplicating entries
            groupBy='areas._pageID',
        }
    )
    
    local out = {}
    local cats = {}
    
    for _, row in ipairs(results) do
        out[#out+1] = row['areas.infobox_html']
        if row['areas.mainpage_categories'] ~= '' then
            for _, cat in ipairs(m_util.string.split(row['areas.mainpage_categories'], ',')) do
                cats[#cats+1] = string.gsub(cat, '[Cc]ategory:', '')
            end
        end
    end
    if tpl_args.cats then
        return table.concat(out) .. m_util.misc.add_category(cats)
    else
        return table.concat(out)
    end
end

return p