Module:Monster: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
>Illviljan
(Append life stats from modifiers as well.)
(handle invalid page title)
 
(33 intermediate revisions by 5 users not shown)
Line 1: Line 1:
-- ----------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- Imports
--
-- ----------------------------------------------------------------------------
--                        Module:Monster
--  
-- This module implements Template:Monster
-------------------------------------------------------------------------------
 
require('Module:No globals')
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 m_util = require('Module:Util')
local m_game = mw.loadData('Module:Game')
local m_game = require('Module:Game')
 
local f_skill_link = require('Module:Skill link').skill_link
local f_skill_link = require('Module:Skill link').skill_link
local p = {}


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
Line 16: Line 20:
local i18n = {
local i18n = {
     cats = {
     cats = {
         data = 'Monster data'
         data = 'Monster data',
        boss = 'Boss',
     },
     },
     tooltips = {
     tooltips = {
         name = 'Name',
         name = 'Name',
         rarity = 'Rarity',
         rarity = 'Rarity',
         experience_multiplier = 'Base Experience Multiplier',  
         experience_multiplier = 'Base Experience Multiplier',
         health_multiplier = 'Base Health Multiplier',
         health_multiplier = 'Base Health Multiplier',
         damage_multiplier = 'Base Damage Multiplier',
         damage_multiplier = 'Base Damage Multiplier',
Line 43: Line 48:
         metadata_id = 'Metadata id',
         metadata_id = 'Metadata id',
         monster_type_id = 'Monster type id',
         monster_type_id = 'Monster type id',
         areas = 'Areas',
         area = 'Area',
        monster_level = 'Level',
        skills = 'Skills',
        life = 'Life',
        damage = 'Damage',
        aps = 'Attacks per second',
        critical_strike_chance_total = 'Critical strike chance',
        armour = 'Armour rating',
        evasion = 'Evasion rating',
        accuracy = 'Accuracy rating',
        experience = 'Experience',
        summon_life = 'Summon life',
        monster_data = 'Monster data',
 
     },
     },
   
 
     intro = {
     intro = {
         text_with_name = "'''%s''' is the internal id for the [[%s|%s]] [[monster]]. ",
         text_with_name = "'''%s''' is the internal id for the [[%s|%s]] [[monster]]. ",
         text_without_name = "'''%s''' is the internal id of an unnamed [[monster]]. ",
         text_without_name = "'''%s''' is the internal id of an unnamed [[monster]]. ",
     },
     },
   
 
     errors = {
     errors = {
         invalid_rarity_id = 'The rarity id "%s" is invalid. Acceptable values are "normal", "magic", "rare" and "unique".',
         invalid_rarity_id = 'The rarity id "%s" is invalid. Acceptable values are "normal", "magic", "rare" and "unique".',
Line 56: Line 74:
}
}
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Helpers
-- Helper functions
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
local h = {}
local h = {}


function h.add_mod_id(tpl_args, frame, value)
function h.add_mod_id(tpl_args, value)
    --[[
        Add mod ids in an ordered way.
    ]]
 
    if type(value) ~= 'table' then
        value = {value}
    end
 
     for _, id in ipairs(value or {}) do
     for _, id in ipairs(value or {}) do
         if tpl_args._mods[id] == nil then
         if tpl_args._mods[id] == nil then
Line 67: Line 93:
         end
         end
     end
     end
   
 
     return value
     return value
end
end
Line 74: Line 100:
     --[[
     --[[
     Calculates a modified stat.
     Calculates a modified stat.
   
 
     Parameters
     Parameters
     ----------
     ----------
     stats : List
     stats : List
         Associated List with added, increased and more
         Associated List with added, increased, more and less as keys.
        as keys.
 
       
     Examples
     Examples
     --------
     --------
Line 89: Line 114:
     }
     }
     = h.stat_calc(stats)
     = h.stat_calc(stats)
   
 
     ]]
     ]]
     local funcs = {
     local funcs = {
         added=function(stats)
         added=function(stats)
             --[[
             --[[
             Sum the added terms.          
             Sum the added terms.
             ]]
             ]]
             local out = 0  
             local out = 0
             for _, v in ipairs(stats['added']) do
             for _, v in ipairs(stats['added']) do
                 out = v + out
                 out = v + out
Line 104: Line 129:
         increased=function(stats)
         increased=function(stats)
             --[[
             --[[
             Sum the increased terms.  
             Sum the increased terms.
             Values should be in percent.
             Values should be in percent.
             ]]
             ]]
Line 115: Line 140:
         more=function(stats)
         more=function(stats)
             --[[
             --[[
             Calculate the product of the more terms.  
             Calculate the product of the more terms.
             Values should be in percent.
             Values should be in percent.
             ]]
             ]]
Line 121: Line 146:
             for _, v in ipairs(stats['more']) do
             for _, v in ipairs(stats['more']) do
                 out = (1 + v/100) * out
                 out = (1 + v/100) * out
            end
            return out
        end,
        less=function(stats)
            --[[
            Calculate the product of the less terms.
            Values should be in percent.
            Should only be used for stats that defines directions in the
            stat id. Prefer the more function instead.
            ]]
            local out = 1
            for _, v in ipairs(stats['less']) do
                out = (1 - v/100) * out
             end
             end
             return out
             return out
         end,
         end,
     }
     }
   
 
     local out = 1
     local out = 1
     for k, v in pairs(stats) do
     for k, v in pairs(stats) do
Line 134: Line 173:
         end
         end
     end
     end
   
 
     return out
     return out
end
end
Line 143: Line 182:
     ]]
     ]]
     local verbose_funcs = {
     local verbose_funcs = {
         added = function(stats)
         added=function(stats)
             local st = {}
             local st = {}
             for _, v in ipairs(stats['added']) do
             for _, v in ipairs(stats['added']) do
                 st[#st+1] = v/1
                 st[#st+1] = v/1
             end  
             end
             return string.format('(%s)', table.concat(st, ' + '))
             return string.format('(%s)', table.concat(st, ' + '))
         end,
         end,
         increased = function(stats)
         increased=function(stats)
             local st = {}
             local st = {}
             for _, v in ipairs(stats['increased']) do
             for _, v in ipairs(stats['increased']) do
                 st[#st+1] = v/100  
                 st[#st+1] = v/100
             end  
             end
             return string.format('(1 + %s)', table.concat(st, ' + '))
             return string.format('(1 + %s)', table.concat(st, ' + '))
         end,
         end,
         more = function(stats)
         more=function(stats)
             local st = {}
             local st = {}
             for _, v in ipairs(stats['more']) do
             for _, v in ipairs(stats['more']) do
                 st[#st+1] = string.format('(1 + %s)', v/100)  
                 st[#st+1] = string.format('(1 + %s)', v/100)
             end  
            end
            return string.format('(%s)', table.concat(st, ' * '))
        end,
        less=function(stats)
            local st = {}
            for _, v in ipairs(stats['less']) do
                st[#st+1] = string.format('(1 - %s)', v/100)
             end
             return string.format('(%s)', table.concat(st, ' * '))
             return string.format('(%s)', table.concat(st, ' * '))
         end,
         end,
     }
     }
   
 
     local out = {}
     local out = {}
     for k, v in pairs(stats) do
     for k, v in pairs(stats) do
Line 174: Line 220:
         end
         end
     end
     end
   
 
     return table.concat(out, ' * ')
     return table.concat(out, ' * ')
end
end
Line 180: Line 226:
function h.stat_match(stats, strings, result)
function h.stat_match(stats, strings, result)
     --[[
     --[[
     Match stats to strings.
     Match strings to ids in result and append them to stats.
      
 
    Parameters
    ----------
    stats : Array, required.
        Array to append values to.
    strings : array, required.
        Array of ids to to match result to.
     result : array, required.
        Row result from a cargo_query. Must contain 'mod_stats.id' and
        'mod_stats.max'.
 
     Examples
     Examples
     --------
     --------
Line 201: Line 257:
     mw.logObject(stats)
     mw.logObject(stats)
     ]]
     ]]
   
 
     for k, stat_ids in pairs(strings) do
     for k, stat_ids in pairs(strings) do
         for _, stat_id in ipairs(stat_ids) do
         for _, stat_id in ipairs(stat_ids) do
             -- Match the stat. TODO: Is there a smarter way? Can't just find  
             -- Match the stat. TODO: Is there a smarter way? Can't just find
             -- the pattern within the string though since increased and more
             -- the pattern within the string though since increased and more
             -- are so similar.
             -- are so similar.
             if stat_id == result['mod_stats.id'] then  
             if stat_id == result['mod_stats.id'] then
                if stats[k] == nil then
                    stats[k] = {}
                end
                 stats[k][#stats[k]+1] = result['mod_stats.max'] -- TODO: add range.
                 stats[k][#stats[k]+1] = result['mod_stats.max'] -- TODO: add range.
             end  
             end
         end
         end
     end
     end
end
end


function h.intro_text(tpl_args, frame)
function h.stat_format(args)
    --[[
    Format the stat value.
 
    Parameters
    ----------
    args : Array, required.
        Array of arguments to modify the stat format. Supported keys are:
            args.fmt
            args.level
            args.value
            args.value_verbose
    ]]
 
    local fmt = '%0.2f'
    if args.value > 10000 then
        fmt = '%0.3E'
    elseif args.value > 100 then
        fmt = '%0.0f'
    end
    return m_util.html.abbr(
        string.format(args.fmt or fmt, args.value),
        string.format('Lvl. %0.0f: %s', args.level, args.value_verbose)
    )
end
 
function h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
    --[[
    Calculate the total stat value per monster level.
 
    Parameters
    ----------
    f_stats : Function, required.
        Function returning an array of stat values per monster level.
    f_strings : function, required.
        Function returning an array of stat ids to match to cargo results per
        monster level.
    ]]
 
    -- Stop if no monster level was found:
    if #tpl_args.monster_level == 0 then
      return nil
    end
 
    -- Calculate the total stat value for each monster level:
    local out = {}
    for i, result in ipairs(tpl_args._mod_data['monster_level']) do
        -- Initial stats for monsters:
        local stats = f_stats(tpl_args, result)
 
        -- Append matching stats from the modifiers:
        for _, modid in ipairs(tpl_args._mods) do
            local mod = tpl_args._mod_data[modid]
            for _, v in ipairs(mod) do
                h.stat_match(
                    stats,
                    f_strings(tpl_args, result),
                    v
                )
            end
        end
 
        -- Calculate the total stat value and format the output:
        out[i] = h.stat_format{
            level=result['monster_base_stats.level'],
            value=h.stat_calc(stats),
            value_verbose=h.stat_calc_verbose(stats),
        }
    end
 
    return table.concat(out, ', ')
end
 
 
function h.intro_text(tpl_args)
     --[[
     --[[
     Display an introductory text about the monster data.
     Display an introductory text about the monster data.
Line 220: Line 353:
     local out = {}
     local out = {}
     if mw.ustring.find(tpl_args['metadata_id'], '_') then
     if mw.ustring.find(tpl_args['metadata_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']}
         }
         }
     end
     end
   
 
     if tpl_args['name'] then
     if tpl_args['name'] then
         out[#out+1] = string.format(
         out[#out+1] = string.format(
             i18n.intro.text_with_name,  
             i18n.intro.text_with_name,
             tpl_args['metadata_id'],  
             tpl_args['metadata_id'],
             tpl_args['main_page'] or tostring(mw.title.getCurrentTitle()),  
             tpl_args['main_page'] or tostring(mw.title.getCurrentTitle()),
             tpl_args['name']
             tpl_args['name']
         )
         )
     else  
     else
         out[#out+1] = string.format(
         out[#out+1] = string.format(
             i18n.intro.text_without_name,
             i18n.intro.text_without_name,
             tpl_args['metadata_id']
             tpl_args['metadata_id']
         )
         )
     end  
     end
   
 
     return table.concat(out)
     return table.concat(out)
end
end


function h.info_box(tpl_args, tbl_view)
    -- Create the infobox:
    local container = mw.html.create('div')
    container
        :attr('class', 'modbox floatright')
    local tbl = container:tag('table')
    tbl
        :attr('class', 'wikitable')
        -- :attr('style', 'float:right; margin-left: 10px;')
    for _, data in ipairs(tbl_view) do
        local v = data.func(tpl_args)
        if v ~= nil and v ~= '' then
            local tr = tbl:tag('tr')
            if data.header then
                tr:tag('th'):wikitext(data.header):done()
                tr:tag('td'):wikitext(v):done()
            else
                tr:tag('th'):attr('colspan', 2):wikitext(v):done()
            end
        end
    end
    return tostring(container)
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', 4)
                :wikitext('Stats')
                :done()
            :done()
        :tag('tr')
            :tag('th')
                :wikitext('#')
                :done()
            :tag('th')
                :wikitext('Stat Id')
                :done()
            :tag('th')
                :wikitext('Min')
                :done()
            :tag('th')
                :wikitext('Max')
                :done()
            :done()
        :done()
    local i = 0
    for _, modid in ipairs(tpl_args._mods) do
        local mod = tpl_args._mod_data[modid]
        for k, v in ipairs(mod) do
            if v['mod_stats.id'] then
                i = i + 1
                local linked_stat = v['mod_stats.id']
                if v['mod_stats._pageName'] then
                    linked_stat = string.format(
                        '[[%s|%s]]',
                        v['mod_stats._pageName'],
                        v['mod_stats.id']
                    )
                end
                tbl
                    :tag('tr')
                        :tag('td')
                            :wikitext(i)
                            :done()
                        :tag('td')
                            :wikitext(linked_stat)
                            :done()
                        :tag('td')
                            :wikitext(v['mod_stats.min'])
                            :done()
                        :tag('td')
                            :wikitext(v['mod_stats.max'])
                            :done()
                        :done()
                    :done()
            end
        end
    end
    return tostring(container)
end


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
Line 251: Line 485:
tables.monsters = {
tables.monsters = {
     table = 'monsters',
     table = 'monsters',
     order = {'metadata_id', 'tags', 'monster_type_id', 'mod_ids', 'part1_mod_ids', 'part2_mod_ids', 'endgame_mod_ids', 'skill_ids', 'name', 'size', 'minimum_attack_distance', 'maximum_attack_distance', 'model_size_multiplier', 'experience_multiplier', 'damage_multiplier', 'health_multiplier', 'critical_strike_chance', 'attack_speed', 'mods', 'is_boss', 'rarity_id', 'rarity'},
     order = {
        'metadata_id',
        'tags',
        'monster_type_id',
        'mod_ids',
        'part1_mod_ids',
        'part2_mod_ids',
        'endgame_mod_ids',
        'skill_ids',
        'name',
        'size',
        'minimum_attack_distance',
        'maximum_attack_distance',
        'model_size_multiplier',
        'experience_multiplier',
        'damage_multiplier',
        'health_multiplier',
        'critical_strike_chance',
        'attack_speed',
        'mods',
        'is_boss',
        'rarity_id',
        'rarity'
    },
     fields = {
     fields = {
         metadata_id = {
         metadata_id = {
             field = 'metadata_id',
             field = 'metadata_id',
             type = 'String',
             type = 'String',
             func = function (tpl_args, frame, value)
             func = function (tpl_args, value)
                 tpl_args.monster_usages = m_cargo.query(
                 tpl_args.monster_usages = m_cargo.query(
                     {'areas', 'maps'},
                     {'areas', 'maps', 'items'},
                     {            
                     {
                         'areas.name',
                         'areas.name',
                         'areas.id',
                         'areas.id',
                         'areas.area_level',
                         'areas.area_level',
                         'areas.boss_monster_ids',
                         'areas.boss_monster_ids',
                        'areas.modifier_ids',
                         'areas.main_page',
                         'areas.main_page',
                         'areas._pageName',
                         'areas._pageName',
                         'maps.area_level',
                         'maps.area_level',
                        'maps._pageName',
                        'items.drop_enabled'
                     },
                     },
                     {
                     {
                         join='areas.id=maps.area_id',
                         join=[[
                         where=string.format('areas.boss_monster_ids HOLDS "%s"', value),  
                            areas.id=maps.area_id,
                            maps._pageID=items._pageID
                        ]],
                         where=m_cargo.replace_holds{
                            string=string.format(
                                [[
                                    CASE WHEN maps.area_level IS NOT NULL THEN
                                        areas.boss_monster_ids HOLDS "%s"
                                        AND items.drop_enabled = True
                                    ELSE
                                        areas.boss_monster_ids HOLDS "%s"
                                    END
                                ]],
                                value,
                                value
                            ),
                            mode='regex'
                        },
                        orderBy='maps.area_level, areas.area_level'
                     }
                     }
                 )
                 )
               
 
                 return value
                 return value
             end,
             end,
Line 280: Line 558:
             field = 'monster_type_id',
             field = 'monster_type_id',
             type = 'String',
             type = 'String',
             func = function (tpl_args, frame, value)
             func = function (tpl_args, value)
                 tpl_args.monster_type = m_cargo.query(
                 tpl_args.monster_type = m_cargo.query(
                     {'monster_types', 'monster_resistances'},
                     {'monster_types', 'monster_resistances'},
Line 289: Line 567:
                         'monster_types.energy_shield_multiplier',
                         'monster_types.energy_shield_multiplier',
                         'monster_types.damage_spread',
                         'monster_types.damage_spread',
                         'monster_resistances.part1_fire',  
                         'monster_resistances.part1_fire',
                         'monster_resistances.part1_cold',  
                         'monster_resistances.part1_cold',
                         'monster_resistances.part1_lightning',  
                         'monster_resistances.part1_lightning',
                         'monster_resistances.part1_chaos',  
                         'monster_resistances.part1_chaos',
                         'monster_resistances.part2_fire',  
                         'monster_resistances.part2_fire',
                         'monster_resistances.part2_cold',  
                         'monster_resistances.part2_cold',
                         'monster_resistances.part2_lightning',  
                         'monster_resistances.part2_lightning',
                         'monster_resistances.part2_chaos',  
                         'monster_resistances.part2_chaos',
                         'monster_resistances.maps_fire',  
                         'monster_resistances.maps_fire',
                         'monster_resistances.maps_cold',  
                         'monster_resistances.maps_cold',
                         'monster_resistances.maps_lightning',  
                         'monster_resistances.maps_lightning',
                         'monster_resistances.maps_chaos'
                         'monster_resistances.maps_chaos'
                     },
                     },
                     {
                     {
                         join='monster_types.monster_resistance_id = monster_resistances.id',
                         join='monster_types.monster_resistance_id = monster_resistances.id',
                         where=string.format('monster_types.id = "%s"', value),  
                         where=string.format('monster_types.id = "%s"', value),
                     }
                     }
                 )[1]
                 )[1] or {}
               
 
                 if tpl_args.monster_type['monster_types.tags'] then
                 if tpl_args.monster_type['monster_types.tags'] then
                     local tags = m_util.string.split(
                     local tags = m_util.string.split(
                         tpl_args.monster_type['monster_types.tags'],  
                         tpl_args.monster_type['monster_types.tags'],
                         ',%s+'
                         ',%s+'
                     )
                     )
                     -- TODO: Maybe this can be fixed earlier?
                     -- TODO: Maybe this can be fixed earlier?
                     if tpl_args.tags == nil then  
                     if tpl_args.tags == nil then
                         tpl_args.tags = {}
                         tpl_args.tags = {}
                     end  
                     end
                     for _, tag in ipairs(tags) do
                     for _, tag in ipairs(tags) do
                         tpl_args.tags[#tpl_args.tags+1] = tag
                         tpl_args.tags[#tpl_args.tags+1] = tag
                     end
                     end
                 end
                 end
               
 
                 return value
                 return value
             end,
             end,
Line 398: Line 676:
             field = 'is_boss',
             field = 'is_boss',
             type = 'Boolean',
             type = 'Boolean',
             func = function (tpl_args, frame)
             func = function (tpl_args)
                 -- If the monster is used in some area it's most likely
                 -- If the monster is used in some area it's most likely
                 -- an unique boss:
                 -- an unique boss:
Line 412: Line 690:
             field = 'rarity_id',
             field = 'rarity_id',
             type = 'String',
             type = 'String',
             func = function (tpl_args, frame)
             func = function (tpl_args)
                 --[[
                 --[[
                     Define the rarity of the monster. There's no obvious  
                     Define the rarity of the monster. There's no obvious
                     parameter that can be datamined for this so this will  
                     parameter that can be datamined for this so this will
                     be mostly guess work.
                     be mostly guess work.
                 ]]
                 ]]
               
 
                 -- User defined rarity takes priority:
                 -- User defined rarity takes priority:
                 if tpl_args.rarity_id ~= nil then  
                 if tpl_args.rarity_id ~= nil then
                     if m_game.constants.rarities[tpl_args.rarity_id] == nil then
                     if m_game.constants.rarities[tpl_args.rarity_id] == nil then
                         error(string.format(i18n.errors.invalid_rarity_id,  
                         error(string.format(i18n.errors.invalid_rarity_id,
                                             tostring(tpl_args.rarity_id)))
                                             tostring(tpl_args.rarity_id)))
                     end
                     end
                     return tpl_args.rarity_id
                     return tpl_args.rarity_id
                 end
                 end
           
 
                 -- If the monster is used in some area it's most likely
                 -- If the monster is used in some area it's most likely
                 -- an unique boss:
                 -- an unique boss:
Line 434: Line 712:
                     return tpl_args.rarity_id
                     return tpl_args.rarity_id
                 end
                 end
               
 
                 -- If there are no mods it's probably a normal monster:
                 -- If there are no mods it's probably a normal monster:
                 if #tpl_args._mods == 0 then
                 if #tpl_args._mods == 0 then
                     tpl_args.rarity_id = 'normal'
                     tpl_args.rarity_id = 'normal'
                     return tpl_args.rarity_id
                     return tpl_args.rarity_id
                 end  
                 end
               
 
                 -- Try to determine rarity from mods:
                 -- Try to determine rarity from mods:
                 for _, modid in ipairs(tpl_args._mods) do
                 for _, modid in ipairs(tpl_args._mods) do
                     local mod = tpl_args._mod_data[modid]
                     local mod = tpl_args._mod_data[modid]
                     -- Check if the mod contains the monster rarity stat:  
                     -- Check if the mod contains the monster rarity stat:
                     for _, v in ipairs(mod) do  
                     for _, v in ipairs(mod) do
                         if v['mod_stats.id'] == 'monster_rarity' then
                         if v['mod_stats.id'] == 'monster_rarity' then
                             -- TODO: m_game rarity id does not match the stat:
                             -- TODO: m_game rarity id does not match the stat:
                             local int_id = tonumber(v['mod_stats.max']) + 1  
                             local int_id = tonumber(v['mod_stats.max']) + 1
                             for k, row in pairs(m_game.constants.rarities) do
                             for k, row in pairs(m_game.constants.rarities) do
                                 if int_id == row['id'] then  
                                 if int_id == row['id'] then
                                     tpl_args.rarity_id = k
                                     tpl_args.rarity_id = k
                                     return tpl_args.rarity_id
                                     return tpl_args.rarity_id
Line 458: Line 736:
                     end
                     end
                 end
                 end
               
 
                 -- If none of the mods contains the monster rarity  
                 -- If none of the mods contains the monster rarity
                 -- stat then it might be an unique:
                 -- stat then it might be an unique:
                 if tpl_args.rarity_id == nil then  
                 if tpl_args.rarity_id == nil then
                     tpl_args.rarity_id = 'unique'
                     tpl_args.rarity_id = 'unique'
                     return tpl_args.rarity_id
                     return tpl_args.rarity_id
Line 470: Line 748:
             field = 'rarity',
             field = 'rarity',
             type = 'String',
             type = 'String',
             func = function (tpl_args, frame)
             func = function (tpl_args)
                return m_game.constants.rarities[tpl_args.rarity_id]['full']
 
            end
                 local results = m_cargo.map_results_to_id{
        },
   
        --
        -- Processing fields
        --
        mods = {
            func = function (tpl_args, frame)
               
                -- Format the mod ids for cargo queries:
                 local mlist = {}
                for _, key in ipairs(tpl_args._mods) do
                    mlist[#mlist+1] = string.format('"%s"', key)
                end
                if #mlist == 0 then
                    return
                end
               
                -- tpl_args._mods = m_cargo.array_query{
                    -- tables={'mods', 'mod_stats'},
                    -- fields={'mods.stat_text', 'mods.generation_type',
                    --        'mod_stats.id', 'mod_stats.max'},
                    -- id_field='mods.id',
                    -- query={join='mods._pageID=mod_stats._pageID'},
                    -- id_array=mlist,
                -- }
               
                tpl_args._mod_data = m_cargo.map_results_to_id{
                     results=m_cargo.query(
                     results=m_cargo.query(
                         {
                         {
                             'mods',  
                             'mods',
                             'mod_stats',
                             'mod_stats',
                         },
                         },
                         {
                         {
                            'mods.domain',
                            'mods.generation_type',
                            'mods.mod_groups',
                             'mods.id',
                             'mods.id',
                            'mods.stat_text',
                            'mods.generation_type',
                             'mod_stats.id',
                             'mod_stats.id',
                             'mod_stats.max',
                             'mod_stats.max',
                            'mod_stats.min',
                            'mod_stats._pageName',
                         },
                         },
                         {
                         {
                             join=[[
                             join='mods._pageID=mod_stats._pageID',
                                mods._pageID=mod_stats._pageID
                            ]],
                             where=string.format([[
                             where=string.format([[
                                 mods.id IN (%s)
                                    mods.domain = 3
                            ]], table.concat(mlist, ',')),
                                AND mods.generation_type = 3
                                 AND mods.id REGEXP "Monster%s[0-9]*$"
                                ]],
                                tpl_args.rarity_id
                            ),
                         }
                         }
                     ),
                     ),
Line 524: Line 780:
                     keep_id_field=false,
                     keep_id_field=false,
                 }
                 }
                for modid, mod in pairs(results) do
                    h.add_mod_id(tpl_args, modid)
                    tpl_args._mod_data[modid] = mod
                end
                return m_game.constants.rarities[tpl_args.rarity_id]['full']
            end
        },
        --
        -- Processing fields
        --
        mods = {
            func = function (tpl_args)
                -- Format the mod ids for cargo queries:
                local mlist = {}
                for _, key in ipairs(tpl_args._mods) do
                    mlist[#mlist+1] = string.format('"%s"', key)
                end
                tpl_args._mod_data = {}
                if #mlist > 0 then
                    tpl_args._mod_data = m_cargo.map_results_to_id{
                        results=m_cargo.query(
                            {
                                'mods',
                                'mod_stats',
                            },
                            {
                                'mods.id',
                                'mods.stat_text',
                                'mods.generation_type',
                                'mod_stats.id',
                                'mod_stats.min',
                                'mod_stats.max',
                                'mod_stats._pageName',
                            },
                            {
                                join=[[
                                    mods._pageID=mod_stats._pageID
                                ]],
                                where=string.format([[
                                    mods.id IN (%s)
                                ]], table.concat(mlist, ',')),
                            }
                        ),
                        field='mods.id',
                        keep_id_field=false,
                    }
                end
             end,
             end,
         },
         },
Line 566: Line 873:
tables.monster_resistances = {
tables.monster_resistances = {
     table = 'monster_resistances',
     table = 'monster_resistances',
     order = {'id', 'part1_fire', 'part1_cold', 'part1_lightning',  
     order = {'id', 'part1_fire', 'part1_cold', 'part1_lightning',
             'part1_chaos', 'part2_fire', 'part2_cold', 'part2_lightning',  
             'part1_chaos', 'part2_fire', 'part2_cold', 'part2_lightning',
             'part2_chaos', 'maps_fire', 'maps_cold', 'maps_lightning',  
             'part2_chaos', 'maps_fire', 'maps_cold', 'maps_lightning',
             'maps_chaos'},
             'maps_chaos'},
     fields = {
     fields = {
Line 628: Line 935:
tables.monster_base_stats = {
tables.monster_base_stats = {
     table = 'monster_base_stats',
     table = 'monster_base_stats',
     order = {'level', 'damage', 'evasion', 'accuracy', 'life', 'experience',  
     order = {'level', 'damage', 'evasion', 'accuracy', 'life', 'experience',
             'summon_life'},
             'summon_life'},
     fields = {
     fields = {
Line 641: Line 948:
         evasion = {
         evasion = {
             field = 'evasion',
             field = 'evasion',
            type = 'Integer',
        },
        armour = {
            field = 'armour',
             type = 'Integer',
             type = 'Integer',
         },
         },
Line 665: Line 976:
tables.monster_map_multipliers = {
tables.monster_map_multipliers = {
     table = 'monster_map_multipliers',
     table = 'monster_map_multipliers',
     order = {'level', 'life', 'damage', 'boss_life', 'boss_damage',  
     order = {'level', 'life', 'damage', 'boss_life', 'boss_damage',
             'boss_item_rarity', 'boss_item_quantity'},
             'boss_item_rarity', 'boss_item_quantity'},
     fields = {
     fields = {
Line 724: Line 1,035:
local display = {}
local display = {}
function display.value (args)
function display.value (args)
     return function (tpl_args, frame)
     return function (tpl_args)
         local v
         local v
         if args.sub then
         if args.sub then
Line 731: Line 1,042:
             v = tpl_args[args.arg]
             v = tpl_args[args.arg]
         end
         end
       
 
         if v and args.fmt then
         if v and args.fmt then
             return string.format(args.fmt, v)
             return string.format(args.fmt, v)
Line 741: Line 1,052:


function display.sub_value (args)
function display.sub_value (args)
     return function (tpl_args, frame)
     return function (tpl_args)
         return tpl_args[args.sub][args.arg]
         return tpl_args[args.sub][args.arg]
     end
     end
Line 749: Line 1,060:
     {
     {
         -- header = i18n.tooltips.name,
         -- header = i18n.tooltips.name,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             if tpl_args.name == nil then  
             if tpl_args.name == nil then
                 return
                 return
             end
             end
           
 
             local linked_name = string.format(
             local linked_name = string.format(
                 '[[Monster:%s|%s]]',  
                 '[[Monster:%s|%s]]',
                 string.gsub(tpl_args.metadata_id, '_', '~'),  
                 string.gsub(tpl_args.metadata_id, '_', '~'),
                 tpl_args.name
                 tpl_args.name
             )
             )
Line 764: Line 1,075:
     {
     {
         -- header = i18n.tooltips.image,
         -- header = i18n.tooltips.image,
         func = function (tpl_args, frame)
         func = function (tpl_args)
            local image_name = tpl_args.name or tpl_args.monster_type_id
            image_name = string.gsub(image_name, '[%[%]]', '')
            local title = mw.title.makeTitle('File', image_name .. ' monster screenshot.jpg')
        if not (title and title.file and title.file.exists) then
        return
        end
             return string.format(
             return string.format(
                 '[[File:%s monster screenshot.jpg|296x500px]]',  
                 '[[File:%s monster screenshot.jpg|296x500px]]',
                 tpl_args.name or tpl_args.monster_type_id
                 image_name
             )
             )
         end,
         end,
Line 776: Line 1,093:
     -- },
     -- },
     {
     {
         header = i18n.tooltips.areas,
         header = i18n.tooltips.area,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             local out = {}
             local out = {}
             for i, v in ipairs(tpl_args.monster_usages) do  
             for i, v in ipairs(tpl_args.monster_usages) do
                 out[#out+1] = string.format(
                 out[#out+1] = string.format(
                     '[[%s|%s]]',  
                     '[[%s|%s]]',
                     v['areas.main_page'] or v['areas._pageName'],  
                     v['areas.main_page'] or v['areas._pageName'],
                     v['areas.name'] or v['areas.id']
                     v['areas.name'] or v['areas.id']
                 )
                 )
             end
             end
           
 
             return table.concat(out, ', ')
             return table.concat(out, ', ')
         end  
        end
    },
    {
        header = i18n.tooltips.monster_level,
        func = function(tpl_args)
            -- Get monster level from the area level unless it's been
            -- user defined.
            local monster_level = {}
            if tpl_args.monster_level then
                monster_level = m_util.string.split(tpl_args.monster_level, ',')
            else
                for _, v in ipairs(tpl_args.monster_usages) do
                    local lvl = v['maps.area_level'] or v['areas.area_level']
                    monster_level[#monster_level+1] = lvl
                end
            end
            tpl_args.monster_level = monster_level
 
            -- Add monster stats specific to monster level:
            if #tpl_args.monster_level > 0 then
                tpl_args._mod_data['monster_level'] = m_cargo.query(
                    {
                        'monster_base_stats',
                        'monster_life_scaling',
                        'monster_map_multipliers',
                    },
                    {
                        'monster_base_stats.level',
 
                        -- Life:
                        'monster_base_stats.life',
                        'monster_life_scaling.magic',
                        'monster_life_scaling.rare',
                        'monster_map_multipliers.life',
                        'monster_map_multipliers.boss_life',
 
                        -- Damage:
                        'monster_base_stats.damage',
                        'monster_map_multipliers.damage',
                        'monster_map_multipliers.boss_damage',
 
                        'monster_base_stats.armour',
                        'monster_base_stats.evasion',
                        'monster_base_stats.accuracy',
                        'monster_base_stats.experience',
                        'monster_base_stats.summon_life',
                    },
                    {
                        join=[[
                            monster_base_stats.level=monster_life_scaling.level,
                            monster_base_stats.level=monster_map_multipliers.level
                        ]],
                        where=string.format(
                            'monster_base_stats.level IN (%s)',
                            table.concat(tpl_args.monster_level, ', ')
                        ),
                    }
                )
            end
 
            return table.concat(tpl_args.monster_level, ', ')
         end
     },
     },
     {
     {
         header = i18n.tooltips.stat_text,
         header = i18n.tooltips.stat_text,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             local out = {}
             local out = {}
             for _, modid in ipairs(tpl_args._mods) do
             for _, modid in ipairs(tpl_args._mods) do
                 local mod = tpl_args._mod_data[modid] or {}
                 local mod = tpl_args._mod_data[modid] or {}
                 local stat_text = {}
                 local stat_text = {}
               
 
                 -- Add stat_text for each modifier, ignore duplicates:
                 -- Add stat_text for each modifier, ignore duplicates:
                 for _, v in ipairs(mod) do  
                 for _, v in ipairs(mod) do
                     if v['mods.stat_text'] then
                     if v['mods.stat_text'] then
                         if stat_text[v['mods.stat_text']] == nil then
                         if stat_text[v['mods.stat_text']] == nil then
Line 808: Line 1,186:
                 end
                 end
             end
             end
           
 
             return table.concat(out, '<br>')
             return table.concat(out, '<br>')
         end,
         end,
     },
     },
     {
     {
         header = 'Skills',
         header = i18n.tooltips.skills,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             local out = {}
             local out = {}
             for _, id in ipairs(tpl_args.skill_ids or {}) do
             for _, id in ipairs(tpl_args.skill_ids or {}) do
                 out[#out+1] = f_skill_link{id=id}
                 out[#out+1] = f_skill_link{id=id}
                 if string.find(out[#out], 'class="module%-error"') then  
                 if string.find(out[#out], 'class="module%-error"') then
                     out[#out] = id
                     out[#out] = id
                 end  
                 end
             end
             end
           
 
             return table.concat(out, '<br>')
             return table.concat(out, '<br>')
         end,
         end,
     },
     },
     {
     {
         header = 'Life',
         header = i18n.tooltips.life,
         func = function(tpl_args, frame)
         func = function(tpl_args)
            -- Should this be stored to cargo?
            local f_stats = function(tpl_args, result)
           
                local stats = {
            -- Get monster level from the area level unless it's been
                    added={
            -- user defined.
                        result['monster_base_stats.life'],
            local monster_level = {}
                    },
            for _, v in ipairs(tpl_args.monster_usages) do
                    increased={
                local lvl = v['maps.area_level'] or v['areas.area_level']
                        result['monster_life_scaling.' .. tpl_args.rarity_id] or 0,
                 monster_level[#monster_level+1] = lvl
                    },
                    -- more={},
                    m_map = (tpl_args.health_multiplier or 1) + (result['monster_map_multipliers.life'] or 0)
                }
                if tpl_args.is_boss then
                    stats.m_map = stats.m_map + (result['monster_map_multipliers.boss_life'] or 0)/100
                 end
 
                return stats
             end
             end
             tpl_args.monster_level = tpl_args.monster_level or table.concat(monster_level, ',')
 
           
             local f_strings = function(tpl_args, result)
            -- Stop if no monster level was found:
                local strings = {
            if tpl_args.monster_level == nil or tpl_args.monster_level == '' then
                    added={'base_maximum_life'},
              return nil
                    increased={'maximum_life_+%', 'map_monsters_life_+%'},
                    more={
                        'maximum_life_+%_final',
                        'monster_life_+%_final_from_rarity',
                    },
                }
                if tpl_args.is_boss then
                    table.insert(strings.increased, 'map_boss_maximum_life_+%')
                end
 
                return strings
             end
             end
           
 
             tpl_args.monster_level = m_util.string.split(tpl_args.monster_level, ',')  
             return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
           
 
            local results = m_cargo.query(
        end,
                {
    },
                    'monster_base_stats',  
    {
                    'monster_life_scaling',
        header = i18n.tooltips.damage,
                    'monster_map_multipliers',  
        func = function(tpl_args)
                    -- 'monster'
            local f_stats = function(tpl_args, result)
                },
                 local stats = {
                 {
                     added={
                     'monster_base_stats.level',
                        result['monster_base_stats.damage'],
                    'monster_base_stats.life',  
                     },
                     'monster_life_scaling.magic',
                     -- increased={},
                    'monster_life_scaling.rare',
                     -- more={},
                    'monster_map_multipliers.life',
                     m_map = (tpl_args.damage_multiplier or 1) + (result['monster_map_multipliers.damage'] or 0)
                    'monster_map_multipliers.boss_life',
                     -- 'monster.health_multiplier',
                },
                {
                     join=[[
                        monster_base_stats.level=monster_life_scaling.level,
                        monster_base_stats.level=monster_map_multipliers.level
                     ]],
                    where=string.format(
                        'monster_base_stats.level IN (%s)',
                        table.concat(tpl_args.monster_level, ', ')
                    ),
                 }
                 }
            )
                if tpl_args.is_boss then
           
                    stats.m_map = stats.m_map + (result['monster_map_multipliers.boss_damage'] or 0)
            -- Add monster modifiers. Min-max range necessary? Do item2
                end
            -- have a smarter way to set mods and stats?
 
            local results2 = m_cargo.query(
                 return stats
                {
            end
                    'mods',
 
                    'mod_stats',
            local f_strings = function(tpl_args, result)
                },
                local strings = {
                 {
                     -- added={},
                    'mods.domain',
                     increased={'map_monsters_damage_+%'},
                    'mods.generation_type',
                     more={'monster_rarity_damage_+%_final'},
                    'mods.mod_group',
                     less={
                    'mods.id',
                         'monster_rarity_attack_cast_speed_+%_and_damage_-%_final',
                     'mod_stats.id',
                         'monster_base_type_attack_cast_speed_+%_and_damage_-%_final',
                     'mod_stats.max',
                     },
                    'mod_stats.min',
                },
                {
                     join='mods._pageID=mod_stats._pageID',
                     where=string.format([[
                            mods.domain = 3
                         AND mods.generation_type = 3
                        AND mods.id LIKE "Monster%s%%"
                         AND mod_stats.id = "monster_life_+%%_final_from_rarity"
                        ]],  
                        tpl_args.rarity_id
                     ),
                 }
                 }
             )
                if tpl_args.is_boss then
            local mods = results2[1] or {}
                    table.insert(strings.increased, 'map_boss_damage_+%')
           
                end
             local out = {}
 
            for i, result in ipairs(results) do
                return strings
                -- Initial stats for monsters:
             end
 
            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.aps,
        func = function(tpl_args)
             local f_stats = function(tpl_args, result)
                 local stats = {
                 local stats = {
                     added={
                     added={
                         result['monster_base_stats.life'],
                         tpl_args.attack_speed or 1,
                    }
                    ,
                    increased={
                        result['monster_life_scaling.' .. tpl_args.rarity_id] or 0,
                     },
                     },
                }
                return stats
            end
            local f_strings = function(tpl_args, result)
                local strings = {
                    increased={'map_monsters_attack_speed_+%'}, -- map_monsters_cast_speed_+%
                     more={
                     more={
                         mods['mod_stats.max'] or 0,
                         'monster_rarity_attack_cast_speed_+%_and_damage_-%_final',
                        'monster_base_type_attack_cast_speed_+%_and_damage_-%_final',
                     },
                     },
                    m1 = tpl_args.health_multiplier or 1,
                    m2 = (result['monster_map_multipliers.life'] or 100)/100,
                    m3 = (result['monster_map_multipliers.boss_life'] or 100)/100,
                 }
                 }
                  
                 if tpl_args.is_boss then
                -- Append matching stats from the modifiers:
                     table.insert(strings.increased, 'map_boss_attack_and_cast_speed_+%')
                for _, modid in ipairs(tpl_args._mods) do
                     local mod = tpl_args._mod_data[modid]
                    for _, v in ipairs(mod) do
                        h.stat_match(
                            stats,
                            {
                                added={'base_maximum_life'},
                                increased={'maximum_life_+%'},
                                more={'maximum_life_+%_final'},
                            },
                            v
                        )
                    end
                 end
                 end
                  
 
                 -- Calculate the total stat value:
                return strings
                 local life = h.stat_calc(stats)
            end
                 local life_verb = h.stat_calc_verbose(stats)
 
                  
            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
                -- Format the output:
        end,
                 out[i] = m_util.html.abbr(
    },
                     string.format('%0.0f', life),  
    {
                    string.format(
        header = i18n.tooltips.critical_strike_chance_total,
                        'Lvl. %s: %s',
        func = function(tpl_args)
                         result['monster_base_stats.level'],
            local f_stats = function(tpl_args, result)
                        life_verb
                local stats = {
                     )
                    added={
                 )
                        tpl_args.critical_strike_chance,
                 end
                    },
           
                 }
             return table.concat(out, ', ')
                 return stats
            end
 
            local f_strings = function(tpl_args, result)
                 local strings = {
                    increased={'map_monsters_critical_strike_chance_+%'},
                }
 
                return strings
            end
 
            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.armour,
        func = function(tpl_args)
            local f_stats = function(tpl_args, result)
                 local stats = {
                    added={
                        result['monster_base_stats.armour'],
                    },
                }
                return stats
            end
 
            local f_strings = function(tpl_args, result)
                 local strings = {
                    -- more={},
                 }
 
                return strings
            end
 
            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.evasion,
        func = function(tpl_args)
            local f_stats = function(tpl_args, result)
                local stats = {
                     added={
                        result['monster_base_stats.evasion'],
                    },
                }
                return stats
            end
 
            local f_strings = function(tpl_args, result)
                local strings = {
                    -- more={},
                }
 
                return strings
            end
 
            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.accuracy,
        func = function(tpl_args)
            local f_stats = function(tpl_args, result)
                local stats = {
                    added={
                         result['monster_base_stats.accuracy'],
                    },
                }
                return stats
            end
 
            local f_strings = function(tpl_args, result)
                local strings = {
                    increased = {'map_monsters_accuracy_rating_+%'},
                     -- more={},
                 }
 
                 return strings
            end
 
             return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
         end,
         end,
     },
     },
     {
     {
         header = i18n.tooltips.resistances,
         header = i18n.tooltips.resistances,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             local tbl = mw.html.create('table')
             local tbl = mw.html.create('table')
             tbl
             tbl
Line 988: Line 1,437:
                         :done()
                         :done()
                     :done()
                     :done()
           
 
             local difficulties = {'part1', 'part2', 'maps'}
             local difficulties = {'part1', 'part2', 'maps'}
             local elements = {'fire', 'cold', 'lightning', 'chaos'}
             local elements = {'fire', 'cold', 'lightning', 'chaos'}
Line 999: Line 1,448:
                 for _, element in ipairs(elements) do
                 for _, element in ipairs(elements) do
                     local field = string.format(
                     local field = string.format(
                         'monster_resistances.%s_%s',  
                         'monster_resistances.%s_%s',
                         k,  
                         k,
                         element
                         element
                     )
                     )
Line 1,010: Line 1,459:
                 end
                 end
             end
             end
           
 
             -- -- Compressed resistance table:
             -- -- Compressed resistance table:
             -- local tbl = mw.html.create('table')
             -- local tbl = mw.html.create('table')
Line 1,016: Line 1,465:
             -- local res = {}
             -- local res = {}
             -- for _, element in ipairs(elements) do
             -- for _, element in ipairs(elements) do
                 -- if res[element] == nil then  
                 -- if res[element] == nil then
                     -- res[element] = {}
                     -- res[element] = {}
                 -- end  
                 -- end
                 -- for _, k in ipairs(difficulties) do
                 -- for _, k in ipairs(difficulties) do
                     -- local r = string.format('monster_resistances.%s_%s', k, element)
                     -- local r = string.format('monster_resistances.%s_%s', k, element)
                     -- res[element][#res[element]+1] = m_util.html.abbr(
                     -- res[element][#res[element]+1] = m_util.html.abbr(
                         -- tpl_args.monster_type[r],  
                         -- tpl_args.monster_type[r],
                         -- k
                         -- k
                     -- )
                     -- )
Line 1,032: Line 1,481:
                         -- :done()
                         -- :done()
             -- end
             -- end
           
 
             return tostring(tbl)
             return tostring(tbl)
        end,
    },
    {
        header = i18n.tooltips.experience,
        func = function(tpl_args)
            local f_stats = function(tpl_args, result)
                local stats = {
                    added={
                        result['monster_base_stats.experience'],
                    },
                    m = tpl_args.experience_multiplier,
                }
                return stats
            end
            local f_strings = function(tpl_args, result)
                local strings = {
                    increased={'monster_slain_experience_+%'},
                }
                return strings
            end
            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.summon_life,
        func = function(tpl_args)
            -- Uniques cannot be summoned:
            if tpl_args.rarity_id == 'unique' then
                return nil
            end
            local f_stats = function(tpl_args, result)
                local stats = {
                    added={
                        result['monster_base_stats.summon_life'],
                    },
                    m = tpl_args.experience_multiplier,
                }
                return stats
            end
            local f_strings = function(tpl_args, result)
                local strings = {
                    -- increased={},
                }
                return strings
            end
            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.metadata_id,
        func = display.value{arg='metadata_id'},
    },
}
local tbl_view_detailed = {
    {
        func = function(tpl_args)
            return i18n.tooltips.monster_data
        end,
    },
    {
        header = i18n.tooltips.metadata_id,
        func = display.value{arg='metadata_id'},
    },
    {
        header = i18n.tooltips.monster_type_id,
        func = display.value{arg='monster_type_id'},
    },
    {
        header = i18n.tooltips.tags,
        func = function (tpl_args)
            if tpl_args.tags == nil or #tpl_args.tags == 0 then
                return
            end
          return table.concat(tpl_args.tags, '<br>')
         end,
         end,
     },
     },
Line 1,050: Line 1,583:
     {
     {
         header = i18n.tooltips.attack_speed,
         header = i18n.tooltips.attack_speed,
         func = display.value{arg='attack_speed', fmt='%.3fs',},
         func = display.value{arg='attack_speed', fmt='%.3fs<sup>-1</sup>',},
     },
     },
     {
     {
Line 1,071: Line 1,604:
         header = i18n.tooltips.model_size_multiplier,
         header = i18n.tooltips.model_size_multiplier,
         func = display.value{arg='model_size_multiplier'},
         func = display.value{arg='model_size_multiplier'},
    },
    {
        header = i18n.tooltips.tags,
        func = function (tpl_args, frame)
            if tpl_args.tags == nil or #tpl_args.tags == 0 then
                return
            end
           
          return table.concat(tpl_args.tags, '<br>')
        end,
    },
    {
        header = i18n.tooltips.metadata_id,
        func = display.value{arg='metadata_id'},
    },
    {
        header = i18n.tooltips.monster_type_id,
        func = display.value{arg='monster_type_id'},
     },
     },
}
}
local list_view = {
local list_view = {
}
}


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


p.table_monsters = m_cargo.declare_factory{data=tables.monsters}
local function _monster(tpl_args)
p.table_monster_types = m_cargo.declare_factory{data=tables.monster_types}
p.table_monster_resistances = m_cargo.declare_factory{data=tables.monster_resistances}
p.table_monster_base_stats = m_cargo.declare_factory{data=tables.monster_base_stats}
p.table_monster_map_multipliers = m_cargo.declare_factory{data=tables.monster_map_multipliers}
p.table_monster_life_scaling = m_cargo.declare_factory{data=tables.monster_life_scaling}
 
p.store_data = m_cargo.store_from_lua{tables=tables, module='Monster'}
 
function p.monster(frame)
     --[[
     --[[
     Stores data and display infoboxes of monsters.
     Stores data and display infoboxes of monsters.
   
 
     Example
     Example
     -------
     -------
Line 1,119: Line 1,623:
         monster_type_id='BanditBoss',
         monster_type_id='BanditBoss',
         mod_ids='MonsterAttackBlock30Bypass20, MonsterExileLifeInMerciless_',
         mod_ids='MonsterAttackBlock30Bypass20, MonsterExileLifeInMerciless_',
         tags='red_blood',  
         tags='red_blood',
         skill_ids='Melee, MonsterHeavyStrike',
         skill_ids='Melee, MonsterHeavyStrike',
         name='Calaf, Headstaver',
         name='Calaf, Headstaver',
Line 1,131: Line 1,635:
         critical_strike_chance=5.0,
         critical_strike_chance=5.0,
         attack_speed=1.35,
         attack_speed=1.35,
       
 
         rarity_id = 'unique'
         rarity_id = 'unique'
     }
     }
    = p.monster{
        metadata_id='Metadata/Monsters/Atziri/Atziri',
        monster_type_id='Atziri',
        mod_ids='MonsterAtziriMapBoss, MapMonsterReducedCurseEffect, AtziriReflectCurses, AtziriMinorDamageReflect, MonsterImplicitCannotBeStunned1, CannotBeSlowedBelowValueBosses, TauntImmunityDurationMapBoss',
        tags='red_blood',
        skill_ids='AtziriMirrorImage, AtziriSummonDemons, AtziriStormCall, AtziriStormCallEmpowered, AtziriFlameblast, AtziriFlameblastEmpowered, AtziriSpearThrow, AtziriSpearThrowEmpowered',
        name='Atziri, Queen of the Vaal',
        size=4,
        minimum_attack_distance=4,
        maximum_attack_distance=16,
        model_size_multiplier=1.65,
        experience_multiplier=2.0,
        damage_multiplier=2.5,
        health_multiplier=9.36,
        critical_strike_chance=5.0,
        attack_speed=1.5,
    }
     ]]
     ]]
   
 
    -- Get args
    tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
   
     tpl_args._mods = {}
     tpl_args._mods = {}
       
 
     -- Parse and store the monster table:
     -- Parse and store the monster table:
     m_cargo.parse_field_arguments{
     m_util.args.from_cargo_map{
         tpl_args=tpl_args,
         tpl_args=tpl_args,
        frame=frame,
         table_map=tables.monsters,
         table_map=tables.monsters,
     }
     }
       
 
     -- Create the infobox:
     -- Attach to table
     local tbl = mw.html.create('table')
     mw.getCurrentFrame():expandTemplate{title = 'Template:Monster/cargo/monsters/attach'}
    tbl
 
        :attr('class', 'wikitable')
     -- Create the infoboxes:
        :attr('style', 'float:right; margin-left: 10px;')
       
     for _, data in ipairs(tbl_view) do
        local v = data.func(tpl_args, frame)
       
        if v ~= nil and v ~= '' then
            local tr = tbl:tag('tr')
            if data.header then
                tr:tag('th'):wikitext(data.header):done()
                tr:tag('td'):wikitext(v):done()
            else
                tr:tag('th'):attr('colspan', 2):wikitext(v):done()
            end
        end
    end
   
     local out = {
     local out = {
         tostring(tbl),
         h.info_box(tpl_args, tbl_view),
         h.intro_text(tpl_args, frame),
        h.info_box(tpl_args, tbl_view_detailed),
        h.stat_box(tpl_args),
         h.intro_text(tpl_args),
     }
     }
     for _, data in ipairs(list_view) do
     for _, data in ipairs(list_view) do
         out[#out+1] = data.func(tpl_args, frame)
         out[#out+1] = data.func(tpl_args)
     end
     end
      
 
     -- Categories:
     local cats = {
     local cats = {
         i18n.cats.data,
         i18n.cats.data,
     }
     }
    local cats_type
    if tpl_args.is_boss then
        cats_type = i18n.cats.boss
    else
        cats_type = tpl_args.rarity
    end
    cats[#cats+1] = string.format('%s %s', cats_type, string.lower(i18n.cats.data))
     return table.concat(out) .. m_util.misc.add_category(cats)
     return table.concat(out) .. m_util.misc.add_category(cats)
end
end
-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------
local p = {}
p.table_monsters = m_cargo.declare_factory{data=tables.monsters}
p.table_monster_types = m_cargo.declare_factory{data=tables.monster_types}
p.table_monster_resistances = m_cargo.declare_factory{data=tables.monster_resistances}
p.table_monster_base_stats = m_cargo.declare_factory{data=tables.monster_base_stats}
p.table_monster_map_multipliers = m_cargo.declare_factory{data=tables.monster_map_multipliers}
p.table_monster_life_scaling = m_cargo.declare_factory{data=tables.monster_life_scaling}
p.store_data = m_cargo.store_from_lua{tables=tables, module='Monster'}
--
-- Template:Monster
--
p.monster = m_util.misc.invoker_factory(_monster, {
    wrappers = 'Template:Monster',
})


return p
return p

Latest revision as of 05:44, 4 September 2023

-------------------------------------------------------------------------------
-- 
--                        Module:Monster
-- 
-- This module implements Template:Monster
-------------------------------------------------------------------------------

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

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

local f_skill_link = require('Module:Skill link').skill_link

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

local i18n = {
    cats = {
        data = 'Monster data',
        boss = 'Boss',
    },
    tooltips = {
        name = 'Name',
        rarity = 'Rarity',
        experience_multiplier = 'Base Experience Multiplier',
        health_multiplier = 'Base Health Multiplier',
        damage_multiplier = 'Base Damage Multiplier',
        attack_speed = 'Base Attack Speed',
        critical_strike_chance = 'Base Critical Strike Chance',
        minimum_attack_distance = 'Minimum Attack Distance',
        maximum_attack_distance = 'Maximum Attack Distance',
        difficulty = 'Act',
        resistances = 'Resistances',
        part1 = '[[Act 1|1]]-[[Act 5|5]]',
        part2 = '[[Act 5|5]]-[[Act 10|10]]',
        maps = '[[Act 10|10]]-',
        fire = m_game.constants.damage_types.fire.short_upper,
        cold = m_game.constants.damage_types.cold.short_upper,
        lightning = m_game.constants.damage_types.lightning.short_upper,
        chaos = m_game.constants.damage_types.chaos.short_upper,
        stat_text = 'Effects from modifiers',
        size = 'Object size',
        model_size_multiplier = 'Model size multiplier',
        tags = 'Internal tags',
        metadata_id = 'Metadata id',
        monster_type_id = 'Monster type id',
        area = 'Area',
        monster_level = 'Level',
        skills = 'Skills',
        life = 'Life',
        damage = 'Damage',
        aps = 'Attacks per second',
        critical_strike_chance_total = 'Critical strike chance',
        armour = 'Armour rating',
        evasion = 'Evasion rating',
        accuracy = 'Accuracy rating',
        experience = 'Experience',
        summon_life = 'Summon life',
        monster_data = 'Monster data',

    },

    intro = {
        text_with_name = "'''%s''' is the internal id for the [[%s|%s]] [[monster]]. ",
        text_without_name = "'''%s''' is the internal id of an unnamed [[monster]]. ",
    },

    errors = {
        invalid_rarity_id = 'The rarity id "%s" is invalid. Acceptable values are "normal", "magic", "rare" and "unique".',
    },
}
-- ----------------------------------------------------------------------------
-- Helper functions
-- ----------------------------------------------------------------------------
local h = {}

function h.add_mod_id(tpl_args, value)
    --[[
        Add mod ids in an ordered way.
    ]]

    if type(value) ~= 'table' then
        value = {value}
    end

    for _, id in ipairs(value or {}) do
        if tpl_args._mods[id] == nil then
            tpl_args._mods[id] = true
            tpl_args._mods[#tpl_args._mods+1] = id
        end
    end

    return value
end

function h.stat_calc(stats)
    --[[
    Calculates a modified stat.

    Parameters
    ----------
    stats : List
        Associated List with added, increased, more and less as keys.

    Examples
    --------
    stats = {
        added={6,4},
        increased={100, 50}, -- [%]
        more={20, 30}, -- [%]
    }
    = h.stat_calc(stats)

    ]]
    local funcs = {
        added=function(stats)
            --[[
            Sum the added terms.
            ]]
            local out = 0
            for _, v in ipairs(stats['added']) do
                out = v + out
            end
            return out
        end,
        increased=function(stats)
            --[[
            Sum the increased terms.
            Values should be in percent.
            ]]
            local out = 1
            for _, v in ipairs(stats['increased']) do
                out = v/100 + out
            end
            return out
        end,
        more=function(stats)
            --[[
            Calculate the product of the more terms.
            Values should be in percent.
            ]]
            local out = 1
            for _, v in ipairs(stats['more']) do
                out = (1 + v/100) * out
            end
            return out
        end,
        less=function(stats)
            --[[
            Calculate the product of the less terms.
            Values should be in percent.

            Should only be used for stats that defines directions in the
            stat id. Prefer the more function instead.
            ]]
            local out = 1
            for _, v in ipairs(stats['less']) do
                out = (1 - v/100) * out
            end
            return out
        end,
    }

    local out = 1
    for k, v in pairs(stats) do
        if type(funcs[k]) == 'function' then
            out = funcs[k](stats) * out
        else
            out = v * out
        end
    end

    return out
end

h.stat_calc_verbose = function(stats)
    --[[
    Show how the stats list is calculated.
    ]]
    local verbose_funcs = {
        added=function(stats)
            local st = {}
            for _, v in ipairs(stats['added']) do
                st[#st+1] = v/1
            end
            return string.format('(%s)', table.concat(st, ' + '))
        end,
        increased=function(stats)
            local st = {}
            for _, v in ipairs(stats['increased']) do
                st[#st+1] = v/100
            end
            return string.format('(1 + %s)', table.concat(st, ' + '))
        end,
        more=function(stats)
            local st = {}
            for _, v in ipairs(stats['more']) do
                st[#st+1] = string.format('(1 + %s)', v/100)
            end
            return string.format('(%s)', table.concat(st, ' * '))
        end,
        less=function(stats)
            local st = {}
            for _, v in ipairs(stats['less']) do
                st[#st+1] = string.format('(1 - %s)', v/100)
            end
            return string.format('(%s)', table.concat(st, ' * '))
        end,
    }

    local out = {}
    for k, v in pairs(stats) do
        if type(verbose_funcs[k]) == 'function' then
            out[#out+1] = verbose_funcs[k](stats)
        else
            out[#out+1] = string.format('%s', v)
        end
    end

    return table.concat(out, ' * ')
end

function h.stat_match(stats, strings, result)
    --[[
    Match strings to ids in result and append them to stats.

    Parameters
    ----------
    stats : Array, required.
        Array to append values to.
    strings : array, required.
        Array of ids to to match result to.
    result : array, required.
        Row result from a cargo_query. Must contain 'mod_stats.id' and
        'mod_stats.max'.

    Examples
    --------
    stats = {
        added={2},
        increased={0},
        more={0},
    }
    strings = {
        added={'base_maximum_life'},
        increased={'maximum_life_+%'},
        more={'maximum_life_+%_final'},
    }
    result = {
        ['mod_stats.id'] = 'maximum_life_+%',
        ['mod_stats.max'] = 100,
    }
    h.stat_match(stats, strings, result)
    mw.logObject(stats)
    ]]

    for k, stat_ids in pairs(strings) do
        for _, stat_id in ipairs(stat_ids) do
            -- Match the stat. TODO: Is there a smarter way? Can't just find
            -- the pattern within the string though since increased and more
            -- are so similar.
            if stat_id == result['mod_stats.id'] then
                if stats[k] == nil then
                    stats[k] = {}
                end
                stats[k][#stats[k]+1] = result['mod_stats.max'] -- TODO: add range.
            end
        end
    end
end

function h.stat_format(args)
    --[[
    Format the stat value.

    Parameters
    ----------
    args : Array, required.
        Array of arguments to modify the stat format. Supported keys are:
            args.fmt
            args.level
            args.value
            args.value_verbose
    ]]

    local fmt = '%0.2f'
    if args.value > 10000 then
        fmt = '%0.3E'
    elseif args.value > 100 then
        fmt = '%0.0f'
    end
    return m_util.html.abbr(
        string.format(args.fmt or fmt, args.value),
        string.format('Lvl. %0.0f: %s', args.level, args.value_verbose)
    )
end

function h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
    --[[
    Calculate the total stat value per monster level.

    Parameters
    ----------
    f_stats : Function, required.
        Function returning an array of stat values per monster level.
    f_strings : function, required.
        Function returning an array of stat ids to match to cargo results per
        monster level.
    ]]

    -- Stop if no monster level was found:
    if #tpl_args.monster_level == 0 then
       return nil
    end

    -- Calculate the total stat value for each monster level:
    local out = {}
    for i, result in ipairs(tpl_args._mod_data['monster_level']) do
        -- Initial stats for monsters:
        local stats = f_stats(tpl_args, result)

        -- Append matching stats from the modifiers:
        for _, modid in ipairs(tpl_args._mods) do
            local mod = tpl_args._mod_data[modid]
            for _, v in ipairs(mod) do
                h.stat_match(
                    stats,
                    f_strings(tpl_args, result),
                    v
                )
            end
        end

        -- Calculate the total stat value and format the output:
        out[i] = h.stat_format{
            level=result['monster_base_stats.level'],
            value=h.stat_calc(stats),
            value_verbose=h.stat_calc_verbose(stats),
        }
    end

    return table.concat(out, ', ')
end


function h.intro_text(tpl_args)
    --[[
    Display an introductory text about the monster data.
    ]]
    local out = {}
    if mw.ustring.find(tpl_args['metadata_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['metadata_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['metadata_id']
        )
    end

    return table.concat(out)
end

function h.info_box(tpl_args, tbl_view)
    -- Create the infobox:
    local container = mw.html.create('div')
    container
        :attr('class', 'modbox floatright')

    local tbl = container:tag('table')
    tbl
        :attr('class', 'wikitable')
        -- :attr('style', 'float:right; margin-left: 10px;')

    for _, data in ipairs(tbl_view) do
        local v = data.func(tpl_args)

        if v ~= nil and v ~= '' then
            local tr = tbl:tag('tr')
            if data.header then
                tr:tag('th'):wikitext(data.header):done()
                tr:tag('td'):wikitext(v):done()
            else
                tr:tag('th'):attr('colspan', 2):wikitext(v):done()
            end
        end
    end

    return tostring(container)
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', 4)
                :wikitext('Stats')
                :done()
            :done()
        :tag('tr')
            :tag('th')
                :wikitext('#')
                :done()
            :tag('th')
                :wikitext('Stat Id')
                :done()
            :tag('th')
                :wikitext('Min')
                :done()
            :tag('th')
                :wikitext('Max')
                :done()
            :done()
        :done()

    local i = 0
    for _, modid in ipairs(tpl_args._mods) do
        local mod = tpl_args._mod_data[modid]
        for k, v in ipairs(mod) do
            if v['mod_stats.id'] then
                i = i + 1

                local linked_stat = v['mod_stats.id']
                if v['mod_stats._pageName'] then
                    linked_stat = string.format(
                        '[[%s|%s]]',
                        v['mod_stats._pageName'],
                        v['mod_stats.id']
                    )
                end

                tbl
                    :tag('tr')
                        :tag('td')
                            :wikitext(i)
                            :done()
                        :tag('td')
                            :wikitext(linked_stat)
                            :done()
                        :tag('td')
                            :wikitext(v['mod_stats.min'])
                            :done()
                        :tag('td')
                            :wikitext(v['mod_stats.max'])
                            :done()
                        :done()
                    :done()
            end
        end
    end

    return tostring(container)
end

-- ----------------------------------------------------------------------------
-- Tables
-- ----------------------------------------------------------------------------
local tables = {}

tables.monsters = {
    table = 'monsters',
    order = {
        'metadata_id',
        'tags',
        'monster_type_id',
        'mod_ids',
        'part1_mod_ids',
        'part2_mod_ids',
        'endgame_mod_ids',
        'skill_ids',
        'name',
        'size',
        'minimum_attack_distance',
        'maximum_attack_distance',
        'model_size_multiplier',
        'experience_multiplier',
        'damage_multiplier',
        'health_multiplier',
        'critical_strike_chance',
        'attack_speed',
        'mods',
        'is_boss',
        'rarity_id',
        'rarity'
    },
    fields = {
        metadata_id = {
            field = 'metadata_id',
            type = 'String',
            func = function (tpl_args, value)
                tpl_args.monster_usages = m_cargo.query(
                    {'areas', 'maps', 'items'},
                    {
                        'areas.name',
                        'areas.id',
                        'areas.area_level',
                        'areas.boss_monster_ids',
                        'areas.modifier_ids',
                        'areas.main_page',
                        'areas._pageName',
                        'maps.area_level',
                        'maps._pageName',
                        'items.drop_enabled'
                    },
                    {
                        join=[[
                            areas.id=maps.area_id,
                            maps._pageID=items._pageID
                        ]],
                        where=m_cargo.replace_holds{
                            string=string.format(
                                [[
                                    CASE WHEN maps.area_level IS NOT NULL THEN
                                        areas.boss_monster_ids HOLDS "%s"
                                        AND items.drop_enabled = True
                                    ELSE
                                        areas.boss_monster_ids HOLDS "%s"
                                    END
                                ]],
                                value,
                                value
                            ),
                            mode='regex'
                        },
                        orderBy='maps.area_level, areas.area_level'
                    }
                )

                return value
            end,
        },
        monster_type_id = {
            field = 'monster_type_id',
            type = 'String',
            func = function (tpl_args, value)
                tpl_args.monster_type = m_cargo.query(
                    {'monster_types', 'monster_resistances'},
                    {
                        'monster_types.tags',
                        'monster_types.armour_multiplier',
                        'monster_types.evasion_multiplier',
                        'monster_types.energy_shield_multiplier',
                        'monster_types.damage_spread',
                        'monster_resistances.part1_fire',
                        'monster_resistances.part1_cold',
                        'monster_resistances.part1_lightning',
                        'monster_resistances.part1_chaos',
                        'monster_resistances.part2_fire',
                        'monster_resistances.part2_cold',
                        'monster_resistances.part2_lightning',
                        'monster_resistances.part2_chaos',
                        'monster_resistances.maps_fire',
                        'monster_resistances.maps_cold',
                        'monster_resistances.maps_lightning',
                        'monster_resistances.maps_chaos'
                    },
                    {
                        join='monster_types.monster_resistance_id = monster_resistances.id',
                        where=string.format('monster_types.id = "%s"', value),
                    }
                )[1] or {}

                if tpl_args.monster_type['monster_types.tags'] then
                    local tags = m_util.string.split(
                        tpl_args.monster_type['monster_types.tags'],
                        ',%s+'
                    )
                    -- TODO: Maybe this can be fixed earlier?
                    if tpl_args.tags == nil then
                        tpl_args.tags = {}
                    end
                    for _, tag in ipairs(tags) do
                        tpl_args.tags[#tpl_args.tags+1] = tag
                    end
                end

                return value
            end,
        },
        mod_ids = {
            field = 'mod_ids',
            type = 'List (,) of String',
            func = h.add_mod_id,
        },
        part1_mod_ids = {
            field = 'part1_mod_ids',
            type = 'List (,) of String',
            func = h.add_mod_id,
        },
        part2_mod_ids = {
            field = 'part2_mod_ids',
            type = 'List (,) of String',
            func = h.add_mod_id,
        },
        endgame_mod_ids = {
            field = 'endgame_mod_ids',
            type = 'List (,) of String',
            func = h.add_mod_id,
        },
        tags = {
            field = 'tags',
            type = 'List (,) of String',
        },
        skill_ids = {
            field = 'skill_ids',
            type = 'List (,) of String',
            --TODO
        },
        -- add base type info or just parse it?
        name = {
            field = 'name',
            type = 'String',
        },
        size = {
            field = 'size',
            type = 'Integer',
        },
        minimum_attack_distance = {
            field = 'minimum_attack_distance',
            type = 'Integer',
        },
        maximum_attack_distance = {
            field = 'maximum_attack_distance',
            type = 'Integer',
        },
        model_size_multiplier = {
            field = 'model_size_multiplier',
            type = 'Float',
        },
        experience_multiplier = {
            field = 'experience_multiplier',
            type = 'Float',
        },
        damage_multiplier = {
            field = 'damage_multiplier',
            type = 'Float',
        },
        health_multiplier = {
            field = 'health_multiplier',
            type = 'Float',
        },
        critical_strike_chance = {
            field = 'critical_strike_chance',
            type = 'Float',
        },
        attack_speed = {
            field = 'attack_speed',
            type = 'Float',
        },
        is_boss = {
            field = 'is_boss',
            type = 'Boolean',
            func = function (tpl_args)
                -- If the monster is used in some area it's most likely
                -- an unique boss:
                if #tpl_args.monster_usages > 0 then
                    tpl_args.is_boss = true
                else
                    tpl_args.is_boss = false
                end
                return tpl_args.is_boss
            end,
        },
        rarity_id = {
            field = 'rarity_id',
            type = 'String',
            func = function (tpl_args)
                --[[
                    Define the rarity of the monster. There's no obvious
                    parameter that can be datamined for this so this will
                    be mostly guess work.
                ]]

                -- User defined rarity takes priority:
                if tpl_args.rarity_id ~= nil then
                    if m_game.constants.rarities[tpl_args.rarity_id] == nil then
                        error(string.format(i18n.errors.invalid_rarity_id,
                                            tostring(tpl_args.rarity_id)))
                    end
                    return tpl_args.rarity_id
                end

                -- If the monster is used in some area it's most likely
                -- an unique boss:
                if tpl_args.is_boss then
                    tpl_args.rarity_id = 'unique'
                    return tpl_args.rarity_id
                end

                -- If there are no mods it's probably a normal monster:
                if #tpl_args._mods == 0 then
                    tpl_args.rarity_id = 'normal'
                    return tpl_args.rarity_id
                end

                -- Try to determine rarity from mods:
                for _, modid in ipairs(tpl_args._mods) do
                    local mod = tpl_args._mod_data[modid]
                    -- Check if the mod contains the monster rarity stat:
                    for _, v in ipairs(mod) do
                        if v['mod_stats.id'] == 'monster_rarity' then
                            -- TODO: m_game rarity id does not match the stat:
                            local int_id = tonumber(v['mod_stats.max']) + 1
                            for k, row in pairs(m_game.constants.rarities) do
                                if int_id == row['id'] then
                                    tpl_args.rarity_id = k
                                    return tpl_args.rarity_id
                                end
                            end
                        end
                    end
                end

                -- If none of the mods contains the monster rarity
                -- stat then it might be an unique:
                if tpl_args.rarity_id == nil then
                    tpl_args.rarity_id = 'unique'
                    return tpl_args.rarity_id
                end
            end,
        },
        rarity = {
            field = 'rarity',
            type = 'String',
            func = function (tpl_args)

                local results = m_cargo.map_results_to_id{
                    results=m_cargo.query(
                        {
                            'mods',
                            'mod_stats',
                        },
                        {
                            'mods.domain',
                            'mods.generation_type',
                            'mods.mod_groups',
                            'mods.id',
                            'mod_stats.id',
                            'mod_stats.max',
                            'mod_stats.min',
                            'mod_stats._pageName',
                        },
                        {
                            join='mods._pageID=mod_stats._pageID',
                            where=string.format([[
                                    mods.domain = 3
                                AND mods.generation_type = 3
                                AND mods.id REGEXP "Monster%s[0-9]*$"
                                ]],
                                tpl_args.rarity_id
                            ),
                        }
                    ),
                    field='mods.id',
                    keep_id_field=false,
                }
                for modid, mod in pairs(results) do
                    h.add_mod_id(tpl_args, modid)
                    tpl_args._mod_data[modid] = mod
                end

                return m_game.constants.rarities[tpl_args.rarity_id]['full']
            end
        },

        --
        -- Processing fields
        --
        mods = {
            func = function (tpl_args)

                -- Format the mod ids for cargo queries:
                local mlist = {}
                for _, key in ipairs(tpl_args._mods) do
                    mlist[#mlist+1] = string.format('"%s"', key)
                end

                tpl_args._mod_data = {}
                if #mlist > 0 then
                    tpl_args._mod_data = m_cargo.map_results_to_id{
                        results=m_cargo.query(
                            {
                                'mods',
                                'mod_stats',
                            },
                            {
                                'mods.id',
                                'mods.stat_text',
                                'mods.generation_type',
                                'mod_stats.id',
                                'mod_stats.min',
                                'mod_stats.max',
                                'mod_stats._pageName',
                            },
                            {
                                join=[[
                                    mods._pageID=mod_stats._pageID
                                ]],
                                where=string.format([[
                                    mods.id IN (%s)
                                ]], table.concat(mlist, ',')),
                            }
                        ),
                        field='mods.id',
                        keep_id_field=false,
                    }
                end
            end,
        },
    }
}

tables.monster_types = {
    table = 'monster_types',
    order = {'id', 'tags', 'monster_resistance_id'},
    fields = {
        id = {
            field = 'id',
            type = 'String',
        },
        tags = {
            field = 'tags',
            type = 'List (,) of String',
        },
        monster_resistance_id = {
            field = 'monster_resistance_id',
            type = 'String',
        },
        armour_multiplier = {
            field = 'armour_multiplier',
            type = 'Float',
        },
        evasion_multiplier = {
            field = 'evasion_multiplier',
            type = 'Float',
        },
        energy_shield_multiplier = {
            field = 'energy_shield_multiplier',
            type = 'Float',
        },
        damage_spread = {
            field = 'damage_spread',
            type = 'Float',
        },
    }
}

tables.monster_resistances = {
    table = 'monster_resistances',
    order = {'id', 'part1_fire', 'part1_cold', 'part1_lightning',
             'part1_chaos', 'part2_fire', 'part2_cold', 'part2_lightning',
             'part2_chaos', 'maps_fire', 'maps_cold', 'maps_lightning',
             'maps_chaos'},
    fields = {
        id = {
            field = 'id',
            type = 'String',
        },
        part1_fire = {
            field = 'part1_fire',
            type = 'Integer',
        },
        part1_cold = {
            field = 'part1_cold',
            type = 'Integer',
        },
        part1_lightning = {
            field = 'part1_lightning',
            type = 'Integer',
        },
        part1_chaos = {
            field = 'part1_chaos',
            type = 'Integer',
        },
        part2_fire = {
            field = 'part2_fire',
            type = 'Integer',
        },
        part2_cold = {
            field = 'part2_cold',
            type = 'Integer',
        },
        part2_lightning = {
            field = 'part2_lightning',
            type = 'Integer',
        },
        part2_chaos = {
            field = 'part2_chaos',
            type = 'Integer',
        },
        maps_fire = {
            field = 'maps_fire',
            type = 'Integer',
        },
        maps_cold = {
            field = 'maps_cold',
            type = 'Integer',
        },
        maps_lightning = {
            field = 'maps_lightning',
            type = 'Integer',
        },
        maps_chaos = {
            field = 'maps_chaos',
            type = 'Integer',
        },
    }
}

tables.monster_base_stats = {
    table = 'monster_base_stats',
    order = {'level', 'damage', 'evasion', 'accuracy', 'life', 'experience',
             'summon_life'},
    fields = {
        level = {
            field = 'level',
            type = 'Integer',
        },
        damage = {
            field = 'damage',
            type = 'Float',
        },
        evasion = {
            field = 'evasion',
            type = 'Integer',
        },
        armour = {
            field = 'armour',
            type = 'Integer',
        },
        accuracy = {
            field = 'accuracy',
            type = 'Integer',
        },
        life = {
            field = 'life',
            type = 'Integer',
        },
        experience = {
            field = 'experience',
            type = 'Integer',
        },
        summon_life = {
            field = 'summon_life',
            type = 'Integer',
        },
        -- whole bunch of other values I have no clue about ...
    }
}

tables.monster_map_multipliers = {
    table = 'monster_map_multipliers',
    order = {'level', 'life', 'damage', 'boss_life', 'boss_damage',
             'boss_item_rarity', 'boss_item_quantity'},
    fields = {
        level = {
            field = 'level',
            type = 'Integer',
        },
        life = {
            field = 'life',
            type = 'Integer',
        },
        damage = {
            field = 'damage',
            type = 'Integer',
        },
        boss_life = {
            field = 'boss_life',
            type = 'Integer',
        },
        boss_damage = {
            field = 'boss_damage',
            type = 'Integer',
        },
        boss_item_rarity = {
            field = 'boss_item_rarity',
            type = 'Integer',
        },
        boss_item_quantity = {
            field = 'boss_item_quantity',
            type = 'Integer',
        },
    }
}

tables.monster_life_scaling = {
    table = 'monster_life_scaling',
    order = {'level', 'magic', 'rare'},
    fields = {
        level = {
            field = 'level',
            type = 'Integer',
        },
        magic = {
            field = 'magic',
            type = 'Integer',
        },
        rare = {
            field = 'rare',
            type = 'Integer',
        },
    }
}

-- ----------------------------------------------------------------------------
-- Monster box sections
-- ----------------------------------------------------------------------------

local display = {}
function display.value (args)
    return function (tpl_args)
        local v
        if args.sub then
            v = tpl_args[args.sub][args.arg]
        else
            v = tpl_args[args.arg]
        end

        if v and args.fmt then
            return string.format(args.fmt, v)
        else
            return v
       end
    end
end

function display.sub_value (args)
    return function (tpl_args)
        return tpl_args[args.sub][args.arg]
    end
end

local tbl_view = {
    {
        -- header = i18n.tooltips.name,
        func = function (tpl_args)
            if tpl_args.name == nil then
                return
            end

            local linked_name = string.format(
                '[[Monster:%s|%s]]',
                string.gsub(tpl_args.metadata_id, '_', '~'),
                tpl_args.name
            )
            return m_util.html.poe_color(tpl_args.rarity_id, linked_name)
        end,
    },
    {
        -- header = i18n.tooltips.image,
        func = function (tpl_args)
            local image_name = tpl_args.name or tpl_args.monster_type_id
            image_name = string.gsub(image_name, '[%[%]]', '')
            local title = mw.title.makeTitle('File', image_name .. ' monster screenshot.jpg')
        	if not (title and title.file and title.file.exists) then
        		return
        	end
            return string.format(
                '[[File:%s monster screenshot.jpg|296x500px]]',
                image_name
            )
        end,
    },
    -- {
        -- header = i18n.tooltips.rarity,
        -- func = display.value{arg='rarity'},
    -- },
    {
        header = i18n.tooltips.area,
        func = function(tpl_args)
            local out = {}
            for i, v in ipairs(tpl_args.monster_usages) do
                out[#out+1] = string.format(
                    '[[%s|%s]]',
                    v['areas.main_page'] or v['areas._pageName'],
                    v['areas.name'] or v['areas.id']
                )
            end

            return table.concat(out, ', ')
        end
    },
    {
        header = i18n.tooltips.monster_level,
        func = function(tpl_args)
            -- Get monster level from the area level unless it's been
            -- user defined.
            local monster_level = {}
            if tpl_args.monster_level then
                monster_level = m_util.string.split(tpl_args.monster_level, ',')
            else
                for _, v in ipairs(tpl_args.monster_usages) do
                    local lvl = v['maps.area_level'] or v['areas.area_level']
                    monster_level[#monster_level+1] = lvl
                end
            end
            tpl_args.monster_level = monster_level

            -- Add monster stats specific to monster level:
            if #tpl_args.monster_level > 0 then
                tpl_args._mod_data['monster_level'] = m_cargo.query(
                    {
                        'monster_base_stats',
                        'monster_life_scaling',
                        'monster_map_multipliers',
                    },
                    {
                        'monster_base_stats.level',

                        -- Life:
                        'monster_base_stats.life',
                        'monster_life_scaling.magic',
                        'monster_life_scaling.rare',
                        'monster_map_multipliers.life',
                        'monster_map_multipliers.boss_life',

                        -- Damage:
                        'monster_base_stats.damage',
                        'monster_map_multipliers.damage',
                        'monster_map_multipliers.boss_damage',

                        'monster_base_stats.armour',
                        'monster_base_stats.evasion',
                        'monster_base_stats.accuracy',
                        'monster_base_stats.experience',
                        'monster_base_stats.summon_life',
                    },
                    {
                        join=[[
                            monster_base_stats.level=monster_life_scaling.level,
                            monster_base_stats.level=monster_map_multipliers.level
                        ]],
                        where=string.format(
                            'monster_base_stats.level IN (%s)',
                            table.concat(tpl_args.monster_level, ', ')
                        ),
                    }
                )
            end

            return table.concat(tpl_args.monster_level, ', ')
        end
    },
    {
        header = i18n.tooltips.stat_text,
        func = function (tpl_args)
            local out = {}
            for _, modid in ipairs(tpl_args._mods) do
                local mod = tpl_args._mod_data[modid] or {}
                local stat_text = {}

                -- Add stat_text for each modifier, ignore duplicates:
                for _, v in ipairs(mod) do
                    if v['mods.stat_text'] then
                        if stat_text[v['mods.stat_text']] == nil then
                            stat_text[v['mods.stat_text']] = true
                            out[#out+1] = v['mods.stat_text']
                        end
                    end
                end
            end

            return table.concat(out, '<br>')
        end,
    },
    {
        header = i18n.tooltips.skills,
        func = function (tpl_args)
            local out = {}
            for _, id in ipairs(tpl_args.skill_ids or {}) do
                out[#out+1] = f_skill_link{id=id}
                if string.find(out[#out], 'class="module%-error"') then
                    out[#out] = id
                end
            end

            return table.concat(out, '<br>')
        end,
    },
    {
        header = i18n.tooltips.life,
        func = function(tpl_args)
            local f_stats = function(tpl_args, result)
                local stats = {
                    added={
                        result['monster_base_stats.life'],
                    },
                    increased={
                        result['monster_life_scaling.' .. tpl_args.rarity_id] or 0,
                    },
                    -- more={},
                    m_map = (tpl_args.health_multiplier or 1) + (result['monster_map_multipliers.life'] or 0)
                }
                if tpl_args.is_boss then
                    stats.m_map = stats.m_map + (result['monster_map_multipliers.boss_life'] or 0)/100
                end

                return stats
            end

            local f_strings = function(tpl_args, result)
                local strings = {
                    added={'base_maximum_life'},
                    increased={'maximum_life_+%', 'map_monsters_life_+%'},
                    more={
                        'maximum_life_+%_final',
                        'monster_life_+%_final_from_rarity',
                    },
                }
                if tpl_args.is_boss then
                    table.insert(strings.increased, 'map_boss_maximum_life_+%')
                end

                return strings
            end

            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)

        end,
    },
    {
        header = i18n.tooltips.damage,
        func = function(tpl_args)
            local f_stats = function(tpl_args, result)
                local stats = {
                    added={
                        result['monster_base_stats.damage'],
                    },
                    -- increased={},
                    -- more={},
                    m_map = (tpl_args.damage_multiplier or 1) + (result['monster_map_multipliers.damage'] or 0)
                }
                if tpl_args.is_boss then
                    stats.m_map = stats.m_map + (result['monster_map_multipliers.boss_damage'] or 0)
                end

                return stats
            end

            local f_strings = function(tpl_args, result)
                local strings = {
                    -- added={},
                    increased={'map_monsters_damage_+%'},
                    more={'monster_rarity_damage_+%_final'},
                    less={
                        'monster_rarity_attack_cast_speed_+%_and_damage_-%_final',
                        'monster_base_type_attack_cast_speed_+%_and_damage_-%_final',
                    },
                }
                if tpl_args.is_boss then
                    table.insert(strings.increased, 'map_boss_damage_+%')
                end

                return strings
            end

            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.aps,
        func = function(tpl_args)
            local f_stats = function(tpl_args, result)
                local stats = {
                    added={
                        tpl_args.attack_speed or 1,
                    },
                }
                return stats
            end

            local f_strings = function(tpl_args, result)
                local strings = {
                    increased={'map_monsters_attack_speed_+%'}, -- map_monsters_cast_speed_+%
                    more={
                        'monster_rarity_attack_cast_speed_+%_and_damage_-%_final',
                        'monster_base_type_attack_cast_speed_+%_and_damage_-%_final',
                    },
                }
                if tpl_args.is_boss then
                    table.insert(strings.increased, 'map_boss_attack_and_cast_speed_+%')
                end

                return strings
            end

            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.critical_strike_chance_total,
        func = function(tpl_args)
            local f_stats = function(tpl_args, result)
                local stats = {
                    added={
                        tpl_args.critical_strike_chance,
                    },
                }
                return stats
            end

            local f_strings = function(tpl_args, result)
                local strings = {
                    increased={'map_monsters_critical_strike_chance_+%'},
                }

                return strings
            end

            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.armour,
        func = function(tpl_args)
            local f_stats = function(tpl_args, result)
                local stats = {
                    added={
                        result['monster_base_stats.armour'],
                    },
                }
                return stats
            end

            local f_strings = function(tpl_args, result)
                local strings = {
                    -- more={},
                }

                return strings
            end

            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.evasion,
        func = function(tpl_args)
            local f_stats = function(tpl_args, result)
                local stats = {
                    added={
                        result['monster_base_stats.evasion'],
                    },
                }
                return stats
            end

            local f_strings = function(tpl_args, result)
                local strings = {
                    -- more={},
                }

                return strings
            end

            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.accuracy,
        func = function(tpl_args)
            local f_stats = function(tpl_args, result)
                local stats = {
                    added={
                        result['monster_base_stats.accuracy'],
                    },
                }
                return stats
            end

            local f_strings = function(tpl_args, result)
                local strings = {
                    increased = {'map_monsters_accuracy_rating_+%'},
                    -- more={},
                }

                return strings
            end

            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.resistances,
        func = function (tpl_args)
            local tbl = mw.html.create('table')
            tbl
                :attr('class', 'wikitable')
                :tag('tr')
                    :tag('th')
                        :wikitext(i18n.tooltips.difficulty)
                        :attr('rowspan', 2)
                        :done()
                    -- :tag('th')
                        -- :wikitext(i18n.tooltips.resistances)
                        -- :attr('colspan', 4)
                        -- :done()
                    :done()
                :tag('tr')
                    :tag('th')
                        :wikitext(i18n.tooltips.fire)
                        :done()
                    :tag('th')
                        :wikitext(i18n.tooltips.cold)
                        :done()
                    :tag('th')
                        :wikitext(i18n.tooltips.lightning)
                        :done()
                    :tag('th')
                        :wikitext(i18n.tooltips.chaos)
                        :done()
                    :done()

            local difficulties = {'part1', 'part2', 'maps'}
            local elements = {'fire', 'cold', 'lightning', 'chaos'}
            for _, k in ipairs(difficulties) do
                local tr = tbl:tag('tr')
                tr
                    :tag('th')
                        :wikitext(i18n.tooltips[k])
                        :done()
                for _, element in ipairs(elements) do
                    local field = string.format(
                        'monster_resistances.%s_%s',
                        k,
                        element
                    )
                    tr
                        :tag('td')
                            :attr('class', 'tc -' .. element)
                            :wikitext(tpl_args.monster_type[field])
                            :done()
                end
            end

            -- -- Compressed resistance table:
            -- local tbl = mw.html.create('table')
            -- local tr = tbl:tag('tr')
            -- local res = {}
            -- for _, element in ipairs(elements) do
                -- if res[element] == nil then
                    -- res[element] = {}
                -- end
                -- for _, k in ipairs(difficulties) do
                    -- local r = string.format('monster_resistances.%s_%s', k, element)
                    -- res[element][#res[element]+1] = m_util.html.abbr(
                        -- tpl_args.monster_type[r],
                        -- k
                    -- )
                -- end
                -- tr
                    -- :tag('td')
                        -- :attr('class', 'tc -' .. element)
                        -- :wikitext(table.concat(res[element], '/'))
                        -- :done()
            -- end

            return tostring(tbl)
        end,
    },
    {
        header = i18n.tooltips.experience,
        func = function(tpl_args)
            local f_stats = function(tpl_args, result)
                local stats = {
                    added={
                        result['monster_base_stats.experience'],
                    },
                    m = tpl_args.experience_multiplier,
                }
                return stats
            end

            local f_strings = function(tpl_args, result)
                local strings = {
                    increased={'monster_slain_experience_+%'},
                }

                return strings
            end

            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.summon_life,
        func = function(tpl_args)
            -- Uniques cannot be summoned:
            if tpl_args.rarity_id == 'unique' then
                return nil
            end

            local f_stats = function(tpl_args, result)
                local stats = {
                    added={
                        result['monster_base_stats.summon_life'],
                    },
                    m = tpl_args.experience_multiplier,
                }
                return stats
            end

            local f_strings = function(tpl_args, result)
                local strings = {
                    -- increased={},
                }

                return strings
            end

            return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
        end,
    },
    {
        header = i18n.tooltips.metadata_id,
        func = display.value{arg='metadata_id'},
    },

}

local tbl_view_detailed = {
    {
        func = function(tpl_args)
            return i18n.tooltips.monster_data
        end,
    },
    {
        header = i18n.tooltips.metadata_id,
        func = display.value{arg='metadata_id'},
    },
    {
        header = i18n.tooltips.monster_type_id,
        func = display.value{arg='monster_type_id'},
    },
    {
        header = i18n.tooltips.tags,
        func = function (tpl_args)
            if tpl_args.tags == nil or #tpl_args.tags == 0 then
                return
            end

           return table.concat(tpl_args.tags, '<br>')
        end,
    },
    {
        header = i18n.tooltips.experience_multiplier,
        func = display.value{arg='experience_multiplier'},
    },
    {
        header = i18n.tooltips.health_multiplier,
        func = display.value{arg='health_multiplier'},
    },
    {
        header = i18n.tooltips.damage_multiplier,
        func = display.value{arg='damage_multiplier'},
    },
    {
        header = i18n.tooltips.attack_speed,
        func = display.value{arg='attack_speed', fmt='%.3fs<sup>-1</sup>',},
    },
    {
        header = i18n.tooltips.critical_strike_chance,
        func = display.value{arg='critical_strike_chance', fmt='%.2f%%',},
    },
    {
        header = i18n.tooltips.minimum_attack_distance,
        func = display.value{arg='minimum_attack_distance'},
    },
    {
        header = i18n.tooltips.maximum_attack_distance,
        func = display.value{arg='maximum_attack_distance'},
    },
    {
        header = i18n.tooltips.size,
        func = display.value{arg='size'},
    },
    {
        header = i18n.tooltips.model_size_multiplier,
        func = display.value{arg='model_size_multiplier'},
    },
}
local list_view = {
}

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

local function _monster(tpl_args)
    --[[
    Stores data and display infoboxes of monsters.

    Example
    -------
    = p.monster{
        metadata_id='Metadata/Monsters/Bandits/BanditBossHeavyStrike_',
        monster_type_id='BanditBoss',
        mod_ids='MonsterAttackBlock30Bypass20, MonsterExileLifeInMerciless_',
        tags='red_blood',
        skill_ids='Melee, MonsterHeavyStrike',
        name='Calaf, Headstaver',
        size=3,
        minimum_attack_distance=4,
        maximum_attack_distance=5,
        model_size_multiplier=1.15,
        experience_multiplier=1.0,
        damage_multiplier=1.0,
        health_multiplier=1.0,
        critical_strike_chance=5.0,
        attack_speed=1.35,

        rarity_id = 'unique'
    }

    = p.monster{
        metadata_id='Metadata/Monsters/Atziri/Atziri',
        monster_type_id='Atziri',
        mod_ids='MonsterAtziriMapBoss, MapMonsterReducedCurseEffect, AtziriReflectCurses, AtziriMinorDamageReflect, MonsterImplicitCannotBeStunned1, CannotBeSlowedBelowValueBosses, TauntImmunityDurationMapBoss',
        tags='red_blood',
        skill_ids='AtziriMirrorImage, AtziriSummonDemons, AtziriStormCall, AtziriStormCallEmpowered, AtziriFlameblast, AtziriFlameblastEmpowered, AtziriSpearThrow, AtziriSpearThrowEmpowered',
        name='Atziri, Queen of the Vaal',
        size=4,
        minimum_attack_distance=4,
        maximum_attack_distance=16,
        model_size_multiplier=1.65,
        experience_multiplier=2.0,
        damage_multiplier=2.5,
        health_multiplier=9.36,
        critical_strike_chance=5.0,
        attack_speed=1.5,
    }

    ]]

    tpl_args._mods = {}

    -- Parse and store the monster table:
    m_util.args.from_cargo_map{
        tpl_args=tpl_args,
        table_map=tables.monsters,
    }

    -- Attach to table
    mw.getCurrentFrame():expandTemplate{title = 'Template:Monster/cargo/monsters/attach'}

    -- Create the infoboxes:
    local out = {
        h.info_box(tpl_args, tbl_view),
        h.info_box(tpl_args, tbl_view_detailed),
        h.stat_box(tpl_args),
        h.intro_text(tpl_args),
    }
    for _, data in ipairs(list_view) do
        out[#out+1] = data.func(tpl_args)
    end

    -- Categories:
    local cats = {
        i18n.cats.data,
    }
    local cats_type
    if tpl_args.is_boss then
        cats_type = i18n.cats.boss
    else
        cats_type = tpl_args.rarity
    end
    cats[#cats+1] = string.format('%s %s', cats_type, string.lower(i18n.cats.data))

    return table.concat(out) .. m_util.misc.add_category(cats)
end

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

local p = {}

p.table_monsters = m_cargo.declare_factory{data=tables.monsters}
p.table_monster_types = m_cargo.declare_factory{data=tables.monster_types}
p.table_monster_resistances = m_cargo.declare_factory{data=tables.monster_resistances}
p.table_monster_base_stats = m_cargo.declare_factory{data=tables.monster_base_stats}
p.table_monster_map_multipliers = m_cargo.declare_factory{data=tables.monster_map_multipliers}
p.table_monster_life_scaling = m_cargo.declare_factory{data=tables.monster_life_scaling}

p.store_data = m_cargo.store_from_lua{tables=tables, module='Monster'}

--
-- Template:Monster
-- 
p.monster = m_util.misc.invoker_factory(_monster, {
    wrappers = 'Template:Monster',
})

return p