Module:Passive skill: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
(Turn globals into locals to prevent errors from happening.)
(exclude old regional passives from passive skill box query)
 
(3 intermediate revisions by 2 users not shown)
Line 1: Line 1:
--
-------------------------------------------------------------------------------
-- Module for passive skills
--  
--
--                            Module:Passive skill
--
-- This module implements Template:Passive skill and Template:Passive skill box
-------------------------------------------------------------------------------


local m_util = require('Module:Util')
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')
local m_cargo = require('Module:Cargo')
local getArgs = require('Module:Arguments').getArgs
 
local f_infocard = require('Module:Infocard')._main
local f_infocard = require('Module:Infocard')._main
local p = {}


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
Line 22: Line 23:
         keystone = 'Keystone passive skills',
         keystone = 'Keystone passive skills',
         notable = 'Notable passive skills',
         notable = 'Notable passive skills',
         basic = 'Basic passive skills',
         basic = 'Small passive skills',
         ascendancy_notable = 'Ascendancy notable passive skills',
         ascendancy_notable = 'Ascendancy notable passive skills',
         ascendancy_basic = 'Ascendancy basic passive skills',
         ascendancy_basic = 'Ascendancy small passive skills',
     },
     },
      
      
Line 110: Line 111:
             field = 'icon',
             field = 'icon',
             type = 'Page',
             type = 'Page',
             func = function(tpl_args, frame, value)
             func = function(tpl_args, value)
                 if value then
                 if value then
                     return string.format(i18n.icon_name, value)
                     return string.format(i18n.icon_name, value)
Line 162: Line 163:
             field = 'stat_text',
             field = 'stat_text',
             type = 'Text',
             type = 'Text',
             func = function (tpl_args, frame)
             func = function (tpl_args, value)
                 if tpl_args.stat_text then
                 if tpl_args.stat_text then
                     tpl_args.stat_text_raw = string.gsub(
                     tpl_args.stat_text_raw = string.gsub(
Line 230: Line 231:
         key = 'ascendancy_class',
         key = 'ascendancy_class',
         header = i18n.passive_box_table.ascendancy_class,
         header = i18n.passive_box_table.ascendancy_class,
         display = function (tpl_args, frame, value)
         display = function (tpl_args, value)
             return string.format('[[%s]]', value)
             return string.format('[[%s]]', value)
         end,
         end,
Line 237: Line 238:
         key = 'connections',
         key = 'connections',
         header = i18n.passive_box_table.connections,
         header = i18n.passive_box_table.connections,
         display = function (tpl_args, frame, value)
         display = function (tpl_args, value)
             local results = m_cargo.map_results_to_id{
             local results = m_cargo.map_results_to_id{
                 field='passive_skills.id',
                 field='passive_skills.id',
Line 365: Line 366:
end
end


function h.intro_text(tpl_args, frame)
function h.intro_text(tpl_args)
     --[[
     --[[
     Display an introductory text about the passive skill.
     Display an introductory text about the passive skill.
Line 371: Line 372:
     local out = {}
     local out = {}
     if mw.ustring.find(tpl_args['id'], '_') then
     if mw.ustring.find(tpl_args['id'], '_') then
         out[#out+1] = frame:expandTemplate{
         out[#out+1] = mw.getCurrentFrame():expandTemplate{
             title='Incorrect title',  
             title='Incorrect title',  
             args = {title=tpl_args['id']}  
             args = {title=tpl_args['id']}  
Line 394: Line 395:
end
end


function h.stat_box(tpl_args, frame)
function h.stat_box(tpl_args)
     --[[
     --[[
     Display the stat box.
     Display the stat box.
Line 455: Line 456:
     return tostring(container)
     return tostring(container)
end
end


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Page functions
-- Main functions
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------


local p = {}
local function _passive_skill(tpl_args)
 
-- This way the helper functions can be used in other modules
p.h = h
 
-- Declare cargo tables:
p.table_passive_skills = m_cargo.declare_factory{data=tables.passive_skills}
p.table_passive_skill_stats = m_cargo.declare_factory{data=tables.passive_skill_stats}
 
function p.passive_skill(frame)
     --[[
     --[[
     Stores data and displays a infobox about the passive skill.
     Stores data and displays a infobox about the passive skill.
Line 492: Line 483:
     ]]
     ]]


    -- Get args
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
     -- parse  
     -- parse  
     m_util.args.from_cargo_map{
     m_util.args.from_cargo_map{
         tpl_args=tpl_args,
         tpl_args=tpl_args,
        frame=frame,
         table_map=tables.passive_skills,
         table_map=tables.passive_skills,
     }
     }
Line 510: Line 495:
         m_cargo.store(stat)
         m_cargo.store(stat)
     end
     end
    -- Attach to tables
    mw.getCurrentFrame():expandTemplate{title = 'Template:Passive skill/cargo/passive skills/attach'}
    mw.getCurrentFrame():expandTemplate{title = 'Template:Passive skill/cargo/passive skill stats/attach'}
      
      
     --
     --
Line 540: Line 529:
             local dsp
             local dsp
             if data.display then
             if data.display then
                 dsp = data.display(tpl_args, frame, value)
                 dsp = data.display(tpl_args, value)
             else
             else
                 dsp = value
                 dsp = value
Line 563: Line 552:
     local out = {
     local out = {
         f_infocard(infocard_args),
         f_infocard(infocard_args),
         h.intro_text(tpl_args, frame),
         h.intro_text(tpl_args),
         h.stat_box(tpl_args, frame),
         h.stat_box(tpl_args),
     }
     }
      
      
Line 573: Line 562:
end
end


function p.passive_skill_box(frame)
local function _passive_skill_box(tpl_args)
     --[[
     --[[
     Queries a passive skill and displays it.
     Queries a passive skill and displays it.
Line 582: Line 571:
      
      
     ]]
     ]]
   
    -- Get args
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
      
      
     tpl_args.name = tpl_args.name or tpl_args[1]
     tpl_args.name = tpl_args.name or tpl_args[1]
      
      
     if not tpl_args.q_where and tpl_args.name then
     if not tpl_args.q_where and tpl_args.name then
         tpl_args.q_where = string.format('passive_skills.name="%s" AND passive_skills.stat_text IS NOT NULL', tpl_args.name)
         tpl_args.q_where = string.format('passive_skills.name="%s" AND passive_skills.stat_text IS NOT NULL AND substring(passive_skills.id, 7, 8) not in ("glennach", "haewark_", "lira_art", "valdos_r", "null_reg", "lex_ejor", "lex_prox", "new_vast", "tirns_en")', tpl_args.name)
     elseif not (tpl_args.q_where and not tpl_args.name) then
     elseif not (tpl_args.q_where and not tpl_args.name) then
         error('q_where or name must be specified')
         error('q_where or name must be specified')
Line 662: Line 645:
end
end


function p.passive_skill_link(frame)
-- ----------------------------------------------------------------------------
    --[[
-- Exported functions
    Links a passive skill
-- ----------------------------------------------------------------------------
   
 
    Examples
local p = {}
    --------
 
    = p.passive_skill_link{id='AscendancyAscendant45', format='tablerow'}
-- This way the helper functions can be used in other modules
    ]]
p.h = h -- Currently used by Module:Blight
   
    -- Get args
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
   
    tpl_args.name = tpl_args.name or tpl_args[1]
    if tpl_args.name then
        tpl_args.q_where = string.format('passive_skills.name="%s"', tpl_args.name)
    elseif tpl_args.id then
        tpl_args.q_where = string.format('passive_skills.id="%s"', tpl_args.id)
    elseif tpl_args.q_where then
    else
        error('Either name, id or q_where must be specified')
    end
   
    local results = m_cargo.query(
        {'passive_skills', 'main_pages'},
        {
            'passive_skills._pageName',
            'passive_skills.stat_text',
            'passive_skills.main_page',
            'passive_skills.name',
            'passive_skills.icon',
            'passive_skills.is_keystone',
            'passive_skills.is_notable',
            'passive_skills.ascendancy_class',
            'main_pages._pageName',
        },
        { 
            join='passive_skills.id=main_pages.id',
            where=string.format('(%s) AND passive_skills.stat_text IS NOT NULL', tpl_args.q_where),
            orderBy='passive_skills.stat_text',
            limit=2,
        }
    )
   
    if #results > 1 then
        error('Too many passives found!')
    elseif #results < 1 then
        error('No passives found')
    end
    local passive = results[1]
   
    if tpl_args.format == 'tablerow' then
        local main_page = passive['passive_skills.main_page'] or passive['main_pages._pageName'] or passive['passive_skills.name']
        return string.format(
            '| [[%s|%s]]%s\n| %s',
            main_page,
            passive['passive_skills.name'],
            h.format_passive_icon(passive, h.get_type(passive)),
            passive['passive_skills.stat_text']
        )
    elseif tpl_args.format == nil then
        return
    else
        error(string.format('Invalid return format specified: %s', tpl_args.format))
    end
end


-- Not sure whether we need a more sophisticated variant like item or mod tables here yet
-- Declare cargo tables:
p.table_passive_skills = m_cargo.declare_factory{data=tables.passive_skills}
p.table_passive_skill_stats = m_cargo.declare_factory{data=tables.passive_skill_stats}


function p.passive_skill_table2(frame)
--
    -- Get args
-- Template:Passive skill
    local tpl_args = getArgs(frame, {
--  
        parentFirst = true
p.passive_skill = m_util.misc.invoker_factory(_passive_skill, {
    })
    wrappers = 'Template:Passive skill',
    frame = m_util.misc.get_frame(frame)
})
   
    if tpl_args.q_tables then
        tpl_args.q_tables = tpl_args.q_tables .. ',' .. 'passive_skill_stats'
    else
        tpl_args.q_tables = 'passive_skill_stats'
    end
   
    if tpl_args.q_join then
        tpl_args.q_join = tpl_args.q_join .. ',' .. 'passive_skills._pageID=passive_skill_stats._pageID'
    else
        tpl_args.q_join = 'passive_skills._pageID=passive_skill_stats._pageID'
    end
   
    tpl_args.q_orderBy = 'passive_skills.ascendancy_class IS NULL DESC, passive_skills.is_keystone, passive_skills.is_notable, passive_skills.name'
   
    return m_cargo.table_query{
        tpl_args=tpl_args,
        frame=frame,
        main_table='passive_skills',
        row_unique_fields = {'passive_skills.name'},
        data={
            tables = {
                passive_skill_stats = {
                    join='passive_skills._pageID=passive_skill_stats._pageID',
                },
            },
            -- display data
            {
                args = {'ascendancy'},
                header = i18n.passive_table.ascendancy_class,
                fields = {
                    'passive_skills.ascendancy_class',
                },
                display = function (tpl_args, frame, tr, data)
                    local passive = data[1]
                    if passive['passive_skills.ascendancy_class'] then
                        tr:tag('td')
                            :wikitext(string.format('[[%s]]<br>[[File:%s avatar.png|link=%s]]', passive['passive_skills.ascendancy_class'], passive['passive_skills.ascendancy_class'], passive['passive_skills.ascendancy_class']))
                    else
                        tr:wikitext(m_util.html.td.na{})
                    end
                end,
                order = 0,
                sort_type = 'text',
            },
            {
                args = nil,
                header = i18n.passive_table.name,
                fields = {
                    'passive_skills._pageName',
                    'passive_skills.main_page',
                    'passive_skills.name',
                    'passive_skills.icon',
                    'passive_skills.is_keystone',
                    'passive_skills.is_notable',
                    'passive_skills.ascendancy_class',
                },
                display = function (tpl_args, frame, tr, data)
                    local passive = data[1]
                    local type_key = h.get_type(passive)
                    tr
                        :tag('td')
                            :attr('data-sort-value', passive['passive_skills.name'] .. type_key)
                            :wikitext(string.format('[[%s|%s]]<br>%s', passive['passive_skills.main_page'] or passive['passive_skills.name'], passive['passive_skills.name'], h.format_passive_icon(passive, type_key)))
                            :done()
                end,
                order = 1000,
                sort_type = 'text',
                options = {
                    [7] = {
                        optional=true,
                    },
                },
            },
            {
                arg = {'default', 'stat', 'stats', 'stat_text'},
                header = i18n.passive_table.stats,
                fields = {
                    'passive_skills.stat_text',
                },
                display = function (tpl_args, frame, tr, data)
                    local passive = data[1]
                    local stats, stat_order = h.make_stat_order(data)
                    tr
                        :tag('td')
                            :wikitext(h.stat_page_links(stat_order, stats))
                            :done()
                end,
                order = 1001,
                sort_type = 'text',
            }
        },
    }
end


function p.passive_skill_table(frame)
--
    -- Get args
-- Template:Passive skill box
    local tpl_args = getArgs(frame, {
--  
        parentFirst = true
p.passive_skill_box = m_util.misc.invoker_factory(_passive_skill_box, {
    })
    wrappers = 'Template:Passive skill box',
    frame = m_util.misc.get_frame(frame)
})
   
    tpl_args.ascendancy = m_util.cast.boolean(tpl_args.ascendancy)
   
    local prepend = {
        q_join=true,
    }
   
    local query = {
        join='passive_skills._pageID=passive_skill_stats._pageID',
        limit=5000,
        groupBy='passive_skills._pageID',
    }
    for key, value in pairs(tpl_args) do
        if string.sub(key, 0, 2) == 'q_' then
            if prepend[key] then
                value = ',' .. value
            end
           
            query[string.sub(key, 3)] = value
        end
    end
   
    local results = m_cargo.query(
        {'passive_skills', 'passive_skill_stats'},
        {
            'passive_skills._pageName',
            'passive_skills.main_page',
            'passive_skills.name',
            'passive_skills.stat_text',
            'passive_skills.icon',
            'passive_skills.is_keystone',
            'passive_skills.is_notable',
            'passive_skills.ascendancy_class',
        },
        query
    )
    result_map = m_cargo.map_results_to_id{
        results=results,
        field='passive_skills.name',
        keep_id_field=true,
    }
    for key, rows in pairs(result_map) do
        result_map[key] = h.sort_by_type(rows)
    end
   
    -- header
    local tbl = mw.html.create('table')
    tbl:addClass('wikitable')
    tbl:addClass('sortable')
   
    local tr = tbl:tag('tr')
    if tpl_args.ascendancy then
        tr:tag('th')
            :wikitext(i18n.passive_table.ascendancy_class)
    end
   
    tr
        :tag('th')
            :wikitext(i18n.passive_table.name)
            :done()
        :tag('th')
            :wikitext(i18n.passive_table.stats)
            :done()
   
    -- rows
    local used_names = {}
    for _, passive_row in ipairs(results) do
        local pn = passive_row['passive_skills.name']
        if used_names[pn] == nil then
            local type_results_map = result_map[passive_row['passive_skills.name']]
            used_names[pn] = true
            for _, type_key in ipairs(h.type_order) do
                local type_results = type_results_map[type_key]
                if #type_results > 0 then
                    local row = type_results[1]
                    tr = tbl:tag('tr')
                       
                    if tpl_args.ascendancy then
                        if row['passive_skills.ascendancy_class'] then
                            tr:tag('td')
                                :wikitext(string.format('[[%s]]<br>[[File:%s avatar.png|link=%s]]', row['passive_skills.ascendancy_class'], row['passive_skills.ascendancy_class'], row['passive_skills.ascendancy_class']))
                        else
                            tr:wikitext(m_util.html.td.na{})
                        end
                    end
                   
                    local stats, stat_order = h.make_stat_order(type_results)
                    tr
                        :tag('td')
                            :attr('data-sort-value', row['passive_skills.name'] .. type_key)
                            :wikitext(string.format('[[%s|%s]]<br>%s', row['passive_skills.main_page'] or row['passive_skills.name'], row['passive_skills.name'], h.format_passive_icon(row, type_key)))
                            :done()
                        :tag('td')
                            :wikitext(h.stat_page_links(stat_order, stats))
                            :done()
                end
            end
        end
    end
   
    return tostring(tbl)
end


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------

Latest revision as of 03:32, 9 November 2023

Module documentation[view] [edit] [history] [purge]


Lua logo

This module depends on the following other modules:

Implements {{passive skill}} and {{passive skill box}}.

-------------------------------------------------------------------------------
-- 
--                            Module:Passive skill
-- 
-- This module implements Template:Passive skill and Template:Passive skill box
-------------------------------------------------------------------------------

local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')

local f_infocard = require('Module:Infocard')._main

-- ----------------------------------------------------------------------------
-- Strings
-- ----------------------------------------------------------------------------

local i18n = {
    icon_name = 'File:%s passive skill icon.png',
    
    cats = {
        data = 'Passive skill data',
        
        keystone = 'Keystone passive skills',
        notable = 'Notable passive skills',
        basic = 'Small passive skills',
        ascendancy_notable = 'Ascendancy notable passive skills',
        ascendancy_basic = 'Ascendancy small passive skills',
    },
    
    passive_box = {
        keystone = 'Keystone',
        notable = 'Notable Passive Skill',
        basic = 'Passive Skill',
        ascendancy_notable = 'Ascendancy Notable Passive Skill',
        ascendancy_basic = 'Ascendancy Passive Skill',
    },
    
    intro = {
        text_with_name = "'''%s''' is the internal id for the [[%s|%s]] [[passive skill]]. ",
        text_without_name = "'''%s''' is the internal id of an unnamed [[passive skill]]. ",
    },
    
    passive_box_table = {
        id = 'Id',
        int_id = 'Integer Id',
        flavour_text = 'Flavour Text',
        reminder_text = 'Reminder Text',
        skill_points = 'Skill Points Granted',
        ascendancy_class = 'Ascendancy Class',
        connections = 'Connections',
    },
    
    passive_table = {
        ascendancy_class = 'Ascendancy<br>Class',
        name = 'Name',
        stats = 'Stats',
    },
    
    errors = {
        no_passives_found = 'No passive skills with the given name found',
    },
}

-- ----------------------------------------------------------------------------
-- Cargo
-- ----------------------------------------------------------------------------

local tables = {}

tables.passive_skills = {
    table = 'passive_skills',
    order = {'id', 'int_id', 'name', 'main_page', 'flavour_text', 'reminder_text', 'buff_id', 'skill_points', 'icon', 'ascendancy_class', 'is_keystone', 'is_notable', 'is_multiple_choice_option', 'is_multiple_choice', 'is_icon_only', 'is_jewel_socket', 'is_ascendancy_starting_node', 'stat_text', 'stat_text_raw', 'connections',},
    fields = {
        id = {
            field = 'id',
            type = 'String',
            required = true,
        },
        int_id = {
            field = 'int_id',
            type = 'Integer',
            required = true,
        },
        name = {
            field = 'name',
            type = 'String',
        },
        main_page = {
            field = 'main_page',
            type = 'Page',
        },
        flavour_text = {
            field = 'flavour_text',
            type = 'Text',
        },
        reminder_text = {
            field = 'reminder_text',
            type = 'Text',
        },
        buff_id = {
            field = 'buff_id',
            type = 'String',
        },
        -- TODO: Other buff stuff 
        skill_points = {
            field = 'skill_points',
            type = 'Integer',
            default = 0,
        },
        icon = {
            field = 'icon',
            type = 'Page',
            func = function(tpl_args, value)
                if value then
                    return string.format(i18n.icon_name, value)
                end
            end
        },
        ascendancy_class = {
            field = 'ascendancy_class',
            type = 'String',
        },
        is_keystone = {
            field = 'is_keystone',
            type = 'Boolean',
            default = false,
        },
        is_notable = {
            field = 'is_notable',
            type = 'Boolean',
            default = false,
        },
        is_multiple_choice_option = {
            field = 'is_multiple_choice_option',
            type = 'Boolean',
            default = false,
        },
        is_multiple_choice = {
            field = 'is_multiple_choice',
            type = 'Boolean',
            default = false,
        },
        is_icon_only = {
            field = 'is_icon_only',
            type = 'Boolean',
            default = false,
        },
        is_jewel_socket = {
            field = 'is_jewel_socket',
            type = 'Boolean',
            default = false,
        },
        is_ascendancy_starting_node = {
            field = 'is_ascendancy_starting_node',
            type = 'Boolean',
            default = false,
        },
        stat_text = {
            field = 'stat_text',
            type = 'Text',
        },
        stat_text_raw = {
            field = 'stat_text',
            type = 'Text',
            func = function (tpl_args, value)
                if tpl_args.stat_text then
                    tpl_args.stat_text_raw = string.gsub(
                        -- [[x]] -> x
                        string.gsub(
                            tpl_args.stat_text, '%[%[([^%]|]+)%]%]', '%1'
                        ), 
                        -- [[x|y]] -> y
                        '%[%[[^|]+|([^%]|]+)%]%]', '%1'
                    )
                end
                return tpl_args.stat_text_raw
            end
        },
        -- from the graph file:
        connections = {
            field = 'connections',
            type = 'List (,) of String',
        },
    }
}

tables.passive_skill_stats = {
    table = 'passive_skill_stats',
    fields = {
        id = {
            field = 'id',
            type = 'String',
        },
        value = {
            field = 'value',
            type = 'Integer',
        },
    }
}

local display = {}
display.map_to_property = {'icon', 'is_keystone', 'is_notable', 'ascendancy_class'}
display.tbl = {
    {
        key = 'id',
        header = i18n.passive_box_table.id,
        display = nil,
    },
    {
        key = 'int_id',
        header = i18n.passive_box_table.int_id,
        display = nil,
    },
    {
        css = 'tc -flavour',
        key = 'flavour_text',
        header = i18n.passive_box_table.flavour_text,
        display = nil,
    },
    {
        key = 'reminder_text',
        header = i18n.passive_box_table.reminder_text,
        display = nil,
    },
    {
        key = 'skill_points',
        header = i18n.passive_box_table.skill_points,
        display = nil,
    },
    {
        key = 'ascendancy_class',
        header = i18n.passive_box_table.ascendancy_class,
        display = function (tpl_args, value)
            return string.format('[[%s]]', value)
        end,
    },
    {
        key = 'connections',
        header = i18n.passive_box_table.connections,
        display = function (tpl_args, value)
            local results = m_cargo.map_results_to_id{
                field='passive_skills.id',
                results=m_cargo.array_query{
                    tables={'passive_skills'},
                    fields={'passive_skills.name', 'passive_skills._pageName'},
                    id_array=value,
                    id_field='passive_skills.id',
                    ignore_missing=true,
                }
            }
            
            local ul = mw.html.create('ul')
            for _, key in ipairs(value) do
                local row = results[key]
                if row then
                    row = row[1]
                end
                local text
                if row then
                    text = string.format('[[%s|%s]]', row['passive_skills._pageName'], row['passive_skills.name'] or row['passive_skills._pageName'])
                else
                    text = key
                end
                ul
                    :tag('li')
                        :wikitext(text)
                        :done()
            end
            
            return tostring(ul)
        end,
    },
}

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

local h = {}

function h.format_passive_icon(passive, passive_type)
    if passive['passive_skills.icon'] == nil then
        return ''
    end
    
    local cls = string.format('passive-icon-type__%s', passive_type)
    local main_page = passive['passive_skills.main_page'] or passive['main_pages._pageName'] or passive['passive_skills.name'] or passive['passive_skills.icon']
    local div = mw.html.create('div')
    div:addClass('passive-icon-container')
    div:addClass(cls)
    div:tag('div')
        :addClass('passive-icon-frame')
        :done()
    div:wikitext(
        string.format(
            '[[%s|link=%s]]', 
            passive['passive_skills.icon'], 
            main_page
        )
    )
    
    return tostring(div)
end

function h.make_stat_order(results)
    local stats = {}
    local stat_order = {}
    for _, row in ipairs(results) do 
        local stat = row['passive_skills.stat_text']
        -- Can't show results here that don't have a stat line
        if stat then
            if stats[stat] == nil then
                stats[stat] = {row}
                table.insert(stat_order, stat)
            else
                table.insert(stats[stat], row)
            end
        end
    end
    
    return stats, stat_order
end

function h.stat_page_links(stat_order, stats)
    local out = {}
    for i, key in ipairs(stat_order) do
        local links = {}
        for j, row in ipairs(stats[key]) do
            links[#links+1] = string.format('[[%s|&#91;%s&#93;]]', row['passive_skills._pageName'], j)
        end
        out[i] = string.format('<span class="passive-line">%s <span class="passive-hover">%s</span></span>', key, table.concat(links, ' '))
    end
    
    return table.concat(out, '<hr>')
end

h.type_order = {'basic', 'notable', 'keystone', 'ascendancy_basic', 'ascendancy_notable'}
function h.get_type(passive)
    local key
    if tonumber(passive['passive_skills.is_keystone']) == 1 then
        key = 'keystone'
    elseif tonumber(passive['passive_skills.is_notable']) == 1 then
        key = 'notable'
    else
        key = 'basic'
    end
    
    if passive['passive_skills.ascendancy_class'] ~= nil then
        key = 'ascendancy_' .. key
    end
    
    return key
end

function h.sort_by_type(results)
    local new = {}
    for _, key in ipairs(h.type_order) do
        new[key] = {}
    end
    
    for _, passive in ipairs(results) do
        table.insert(new[h.get_type(passive)], passive)
    end
    
    return new
end

function h.intro_text(tpl_args)
    --[[
    Display an introductory text about the passive skill.
    ]]
    local out = {}
    if mw.ustring.find(tpl_args['id'], '_') then
        out[#out+1] = mw.getCurrentFrame():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 
    
    return table.concat(out)
end

function h.stat_box(tpl_args)
    --[[
    Display the stat box.
    ]]
    local container = mw.html.create('div')
    container
        :attr('class', 'modbox floatright')
        
    -- stat table 
    local tbl = container:tag('table')
    tbl
        :attr('class', 'wikitable sortable')
        -- :attr('style', 'style="width: 100%;"')
        :tag('tr')
            :tag('th')
                :attr('colspan', 3)
                :wikitext('Stats')
                :done()
            :done()
        :tag('tr')
            :tag('th')
                :wikitext('#')
                :done()
            :tag('th')
                :wikitext('Stat Id')
                :done()
            :tag('th')
                :wikitext('Value')
                :done()
            :done()
            :done()
        :done()
        
    local i = 0
    local value = nil
    repeat
        i = i + 1
        value = {
            id = tpl_args[string.format('stat%s_id', i)],
            value = tpl_args[string.format('stat%s_value', i)],
        }
        
        if value.id then
            tbl
                :tag('tr')
                    :tag('td')
                        :wikitext(i)
                        :done()
                    :tag('td')
                        :wikitext(value.id)
                        :done()
                    :tag('td')
                        :wikitext(value.value)
                        :done()
                    :done()
                :done()
        end
    until value.id == nil
    
    return tostring(container)
end

-- ----------------------------------------------------------------------------
-- Main functions
-- ----------------------------------------------------------------------------

local function _passive_skill(tpl_args)
    --[[
    Stores data and displays a infobox about the passive skill.
    
    Examples
    --------
    = p.passive_skill{
        id = 'life_life_leech1629',
        int_id = '27788',
        name = 'Blood Drinker',
        is_notable = 'True',
        icon = 'lifeleech',
        stat1_id = 'maximum_life_+%',
        stat1_value = '8',
        stat2_id = 'base_life_leech_from_attack_damage_permyriad',
        stat2_value = '40',
        stat_text = '8% increased maximum life<br>0.4% of Attack Damage Leeched as Life',
        connections = 'life1415,life1413',
    }
    
    ]]

    -- parse 
    m_util.args.from_cargo_map{
        tpl_args=tpl_args,
        table_map=tables.passive_skills,
    }
    
    -- parse stats
    m_util.args.stats(tpl_args, {})
    for _, stat in ipairs(tpl_args.stats) do
        stat._table = tables.passive_skill_stats.table
        m_cargo.store(stat)
    end

    -- Attach to tables
    mw.getCurrentFrame():expandTemplate{title = 'Template:Passive skill/cargo/passive skills/attach'}
    mw.getCurrentFrame():expandTemplate{title = 'Template:Passive skill/cargo/passive skill stats/attach'}
    
    --
    -- Infobox
    --
    local passive = {}
    for _, key in ipairs(display.map_to_property) do
        local v = tpl_args[key]
        if type(v) == 'boolean' then
            if v then
                v = 1
            else
                v = 0
            end
        end
        passive[string.format('%s.%s', tables.passive_skills.table, tables.passive_skills.fields[key].field)] = v
    end
    
    local type_key = h.get_type(passive)
    
    local infocard_args = {}
    infocard_args.header = tpl_args.name
    infocard_args.subheader = i18n.passive_box[type_key]
    
    local tbl = mw.html.create('table')
    for _, data in ipairs(display.tbl) do
        local value = tpl_args[data.key]
        -- if default is nil, this will be compared against nil which is what we want, so value ~= nil isn't needed
        if value ~= tables.passive_skills.fields[data.key].default then
            local dsp
            if data.display then
                dsp = data.display(tpl_args, value)
            else
                dsp = value
            end
            tbl
                :tag('tr')
                    :tag('th')
                        :wikitext(data.header)
                        :done()
                    :tag('td')
                        :attr('class', data.css)
                        :wikitext(dsp)
                        :done()
                    :done()
        end
    end
    
    infocard_args[1] = tostring(tbl)
    infocard_args[2] = tpl_args.stat_text
    infocard_args[3] = h.format_passive_icon(passive, type_key)

    local out = {
        f_infocard(infocard_args),
        h.intro_text(tpl_args),
        h.stat_box(tpl_args),
    }
    
    local cats = {
        i18n.cats.data,
    }
    return table.concat(out) .. m_util.misc.add_category(cats)
end

local function _passive_skill_box(tpl_args)
    --[[
    Queries a passive skill and displays it.
    
    Examples
    --------
    = p.passive_skill_box{name='Ghost Reaver'}
    
    ]]
    
    tpl_args.name = tpl_args.name or tpl_args[1]
    
    if not tpl_args.q_where and tpl_args.name then
        tpl_args.q_where = string.format('passive_skills.name="%s" AND passive_skills.stat_text IS NOT NULL AND substring(passive_skills.id, 7, 8) not in ("glennach", "haewark_", "lira_art", "valdos_r", "null_reg", "lex_ejor", "lex_prox", "new_vast", "tirns_en")', tpl_args.name)
    elseif not (tpl_args.q_where and not tpl_args.name) then
        error('q_where or name must be specified')
    end
    
    local results = m_cargo.query(
        {'passive_skills'},
        {
            'passive_skills._pageName', 
            'passive_skills.main_page',
            'passive_skills.name', 
            'passive_skills.stat_text',
            -- TODO: only really need these once, maybe put in extra query
            'passive_skills.flavour_text',
            'passive_skills.icon',
            'passive_skills.is_keystone',
            'passive_skills.is_notable',
            'passive_skills.ascendancy_class',
            'passive_skills.id'
        }, 
        {
            where=tpl_args.q_where,
            orderBy='passive_skills.stat_text',
            limit=5000,
        }
    )
    if #results == 0 then
        error(i18n.errors.no_passives_found)
    end
    
    results = h.sort_by_type(results)
    local out = {}
    local cats = {}
    for _, type_key in ipairs(h.type_order) do
        local type_results = results[type_key]
        if #type_results > 0 then
            cats[#cats+1] = i18n.cats[type_key]
            local stats, stat_order = h.make_stat_order(type_results)
            
            local passive = type_results[1]
            
            local infocard_args = {}
            infocard_args.header = passive['passive_skills.name']
            infocard_args.subheader = i18n.passive_box[type_key]
            
            infocard_args[1] = h.format_passive_icon(passive, type_key)
            infocard_args[2] = h.stat_page_links(stat_order, stats)
            infocard_args[3] = m_util.html.poe_color('flavour', passive['passive_skills.flavour_text'])
            
            out[#out+1] = f_infocard(infocard_args)
            
            -- Store as main page:
            for _, v in ipairs(type_results) do
                m_cargo.store(
                    {
                        _table='main_pages', 
                        id=v['passive_skills.id'],
                    }
                )
            end
        end
    end
    
    if tpl_args.cats == nil or m_util.cast.boolean(tpl_args.cats) then
        out[#out+1] = m_util.misc.add_category(cats)
    end
    
    return table.concat(out)
end

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

local p = {}

-- This way the helper functions can be used in other modules
p.h = h -- Currently used by Module:Blight

-- Declare cargo tables:
p.table_passive_skills = m_cargo.declare_factory{data=tables.passive_skills}
p.table_passive_skill_stats = m_cargo.declare_factory{data=tables.passive_skill_stats}

--
-- Template:Passive skill
-- 
p.passive_skill = m_util.misc.invoker_factory(_passive_skill, {
    wrappers = 'Template:Passive skill',
})

--
-- Template:Passive skill box
-- 
p.passive_skill_box = m_util.misc.invoker_factory(_passive_skill_box, {
    wrappers = 'Template:Passive skill box',
})

-- ----------------------------------------------------------------------------
-- End
-- ----------------------------------------------------------------------------

return p