Module:Area/sandbox

-- SMW powered area module

-- -- TODO -- -- invalid tags -> utl? -- spawn_weight* values -- spawnchacne values

-- -- 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

-- -- Localization --

-- Strings local i18n = { images = { waypoint_no = '', waypoint_yes = '', waypoint_town = '', loading_screen = 'File:%s loading screen.png', loading_screen_infobox = '', screenshot = 'File:%s area screenshot.%s', screenshot_infobox = '', },

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', },   intro = { text_with_name = "%s is the internal id for the %s area. ", text_without_name = "%s is the internal id of an unnamed area. ", connections = 'It is connected to the following areas:', },   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', vaal_spawn_areas = 'Spawnable in:' }, }

-- -- Constats --

local c = {}

-- -- Utility & helper functions --

local factory = {}

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, ' ') 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', tpl_args.areas[area_id]['areas.main_page'], tpl_args.areas[area_id]['areas.name']) else return string.format('%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 display = {}

local argument_map = { table = 'areas', order = { 'main_page', 'id', 'name', 'act', 'area_level', 'level_restriction_max', 'area_type_tags', 'tags', 'loading_screen', 'connection_ids', 'parent_area_id', 'modifier_ids', -- populated via modifier ids 'stat_text', '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', -- those four are parsed via the argument above 'strongbox_weight_normal', 'strongbox_weight_magic', 'strongbox_weight_rare', 'strongbox_weight_unique', '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', 'mainpage_categories', -- Non argument, but passed to the script 'areas', },   --    -- User supplied arguments --    fields = { main_page = { field = 'main_page', type = 'Page', func = function(tpl_args, frame, value) if value ~= nil then local page = mw.title.new(value) if page == nil then error(string.format(i18n.errors.main_page_is_invalid, value)) elseif not page.exists then error(string.format(i18n.errors.main_page_does_not_exist, value)) else -- dont need the title object anymore --tpl_args.main_page = page end end return page 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', },       area_level = { field = 'area_level', type = 'Integer', },       level_restriction_max = { field = 'level_restriction_max', type = 'Integer', 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, loading_id) if loading_id ~= nil then tpl_args.loading_screen_infobox = string.format(i18n.images.loading_screen_infobox, loading_id) return string.format(i18n.images.loading_screen, loading_id) end end, },       connection_ids = { field = 'connection_ids', type = 'List of String', default = {}, },       parent_area_id = { field = 'parent_area_id', type = 'String', },       modifier_ids = { field = 'modifier_ids', type = 'List of String', func = function(tpl_args, frame, value) if value == nil then return end tpl_args.mods = m_util.cargo.array_query{ tables={'mods'}, fields={'mods.stat_text'}, id_field='mods.id', id_array=value }               return value end, default = {}, },       stat_text = { field = 'stat_text', type = 'Text', func = function (tpl_args, frame) if tpl_args.mods == nil then return end local text = {} for page, row in pairs(tpl_args.mods) do                   if row['mods.stat_text'] ~= '' then text[#text+1] = row['mods.stat_text'] end end return table.concat(text, ' ') end, },       monster_ids = { field = 'monster_ids', type = 'List of String', default = {}, },       boss_monster_ids = { field = 'boss_monster_ids', type = 'List of String', 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, screenshot_id) if tpl_args.name ~= nil then local name = screenshot_id 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', default = {}, },       vaal_area_spawn_chance = { field = 'vaal_area_spawn_chance', type = 'Integer', default = 0, },       strongbox_spawn_chance = { field = 'strongbox_spawn_chance', type = 'Integer', default = 0, },       strongbox_max_count = { field = 'strongbox_max_count', type = 'Integer', 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 -- will be read later tpl_args.strongbox_rarity_weight[data.long_lower] = value end end, },       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', },       --        -- Area flags --       is_map_area = { field = 'is_map_area', type = 'Boolean', default = false, },       is_unique_map_area = { field = 'is_unique_map_area', type = 'Boolean', default = false, },       is_town_area = { field = 'is_town_area', type = 'Boolean', default = false, },       is_hideout_area = { field = 'is_hideout_area', type = 'Boolean', default = false, },       is_vaal_area = { field = 'is_vaal_area', type = 'Boolean', default = false, },       is_master_daily_area = { field = 'is_is_master_daily_area', type = 'Boolean', default = false, },       is_labyrinth_area = { field = 'is_labyrinth_area', type = 'Boolean', default = false, },       is_labyrinth_airlock_area = { field = 'is_labyrinth_airlock_area', type = 'Boolean', default = false, },       is_labyrinth_boss_area = { field = 'is_labyrinth_boss_area', type = 'Boolean', default = false, },       has_waypoint = { field = 'has_waypoint', type = 'Boolean', default = false, },       mainpage_categories = { field = 'mainpage_categories', type = 'List of String', func = function (tpl_args, frame, value) -- Category handling for main page only by adding the categories to a cargo field: -- 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 return cats end },       --        -- Handled elsewhere --       release_version = { field = 'release_version', type = 'String', skip = true, },       removal_version = { field = 'removal_version', type = 'String', skip = true, },       infobox_html = { field = 'infobox_html', type = 'Text', skip = true, },       --        -- Not argument to the template, but still parsing arguments --       areas = { func = function(tpl_args, frame, value) 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] = true end elseif tpl_args[arg] then query_ids[tpl_args[arg]] = true end end local query_ids_trimmed = {} for k, _ in pairs(query_ids) do                   query_ids_trimmed[#query_ids_trimmed+1] = k                end local results=m_util.cargo.array_query{ tables={'areas'}, fields={'areas._pageName', 'areas.name', 'areas.main_page'}, id_field='areas.id', id_array=query_ids_trimmed, warning_on_missing=true, }               local areas = {} for _, data in ipairs(results) do                   areas[data['areas.id']] = data end if tpl_args.is_vaal_area then local spawn_areas = {} results = m_util.cargo.query(                       {'areas'},                        {'areas._pageName', 'areas.id', 'areas.name', 'areas.main_page'},                        {                            -- TODO using a workaround since HOLDS is bricked                            -- Don't show connected areas without a main_page to avoid showing disabled or areas which are not relevant                            where=string.format( areas.vaal_area_ids__full LIKE "%%%s%%" AND areas.main_page IS NOT NULL AND areas.vaal_area_spawn_chance > 0, tpl_args.id),                            -- area id for story areas basically orders them by appearance, should be good enough                            orderBy='areas.id ASC',                        }                    ) for _, data in ipairs(results) do                       table.insert(spawn_areas, data['areas.id']) areas[data['areas.id']] = data end tpl_args.vaal_spawn_areas = spawn_areas end return areas end, },   }, }

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', 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, },   -- virtual field created by querying other area data to find where a vaal area is used {       args = { is_vaal_area = {}, vaal_spawn_areas = {}, },       header = i18n.headers.vaal_spawn_areas, func = function(tpl_args, frame) return util.display.multiple_areas(tpl_args, frame, tpl_args.vaal_spawn_areas) 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(           i18n.intro.text_with_name,             tpl_args['id'],             tpl_args['main_page'] or tostring(mw.title.getCurrentTitle),             tpl_args['name']        ) else out[#out+1] = string.format(           i18n.intro.text_without_name,            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(                       '%s (%s)',                         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(           i18n.intro.connections .. '%s',             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 --   -- Handle release_version and removal_version m_util.args.version(tpl_args, {frame=frame}) local cargo_values = m_util.args.from_cargo_map{ tpl_args=tpl_args, frame=frame, table_map=argument_map, rtr=true, }   -- Parse spawn weights m_util.args.spawn_weight_list(tpl_args, {       frame=frame,     }) -- 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: cargo_values['infobox_html'] = out[1] -- Set all semantic properties: mw.logObject('Cargo:' .. m_util.cargo.store(frame, cargo_values)) -- 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 = m_util.cargo.query(       {'areas'},        {'areas.infobox_html', 'areas.mainpage_categories'},        {            where=tpl_args.where,            orderBy=tpl_args.order_by,        }    ) 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