Module:Monster: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
>OmegaK2
(Fix error)
(handle invalid page title)
 
(38 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 = require('Module:Game')


local p = {}
local m_game = mw.loadData('Module:Game')
 
local f_skill_link = require('Module:Skill link').skill_link


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
Line 14: Line 19:


local i18n = {
local i18n = {
    cats = {
        data = 'Monster data',
        boss = 'Boss',
    },
     tooltips = {
     tooltips = {
         name = 'Name',
         name = 'Name',
         experience_multiplier = 'Base Experience Multiplier',  
        rarity = 'Rarity',
         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 23: Line 33:
         minimum_attack_distance = 'Minimum Attack Distance',
         minimum_attack_distance = 'Minimum Attack Distance',
         maximum_attack_distance = 'Maximum Attack Distance',
         maximum_attack_distance = 'Maximum Attack Distance',
         difficulty = 'Difficulty',
         difficulty = 'Act',
         resistances = 'Resistances',
         resistances = 'Resistances',
         part1 = '[[Act 1]]-<br>[[Act 5]]',
         part1 = '[[Act 1|1]]-[[Act 5|5]]',
         part2 = '[[Act 5]]-<br>[[Act 10]]',
         part2 = '[[Act 5|5]]-[[Act 10|10]]',
         maps = 'post<br>[[Act 10]]',
         maps = '[[Act 10|10]]-',
         fire = m_game.constants.damage_types.fire.short_upper,
         fire = m_game.constants.damage_types.fire.short_upper,
         cold = m_game.constants.damage_types.cold.short_upper,
         cold = m_game.constants.damage_types.cold.short_upper,
Line 37: Line 47:
         tags = 'Internal tags',
         tags = 'Internal tags',
         metadata_id = 'Metadata id',
         metadata_id = 'Metadata id',
         areas = 'Areas',
         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 = {
     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 44: 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
         tpl_args._mods[id] = true
         if tpl_args._mods[id] == nil then
            tpl_args._mods[id] = true
            tpl_args._mods[#tpl_args._mods+1] = id
        end
     end
     end
   
 
     return value
     return value
end
end
Line 59: 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 74: 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 89: 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 100: 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 106: 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 119: Line 173:
         end
         end
     end
     end
   
 
     return out
     return out
end
end
Line 128: 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 159: Line 220:
         end
         end
     end
     end
   
 
     return table.concat(out, ' * ')
     return table.concat(out, ' * ')
end  
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
-- Tables
Line 169: 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',  
     order = {
    'mods'},
        '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 199: 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 208: 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
                     for _, tag in ipairs(m_util.string.split(tpl_args.monster_type['monster_types.tags'], ',%s+')) do
                     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
                         tpl_args.tags[#tpl_args.tags+1] = tag
                     end
                     end
                 end
                 end
               
 
                 return value
                 return value
             end,
             end,
Line 306: Line 673:
             type = 'Float',
             type = 'Float',
         },
         },
         -- rarity_id = {
         is_boss = {
             -- field = 'rarity_id',
            field = 'is_boss',
             -- type = 'String',
            type = 'Boolean',
             -- func = function (tpl_args, frame)
            func = function (tpl_args)
                 -- if m_game.constants.rarities[tpl_args.rarity_id] == nil then
                -- If the monster is used in some area it's most likely
                    -- error(string.format(i18n.errors.invalid_rarity_id, tostring(tpl_args.rarity_id)))
                -- an unique boss:
                 -- end
                if #tpl_args.monster_usages > 0 then
            -- end
                    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
         -- Processing fields
         --
         --
         mods = {
         mods = {
             func = function (tpl_args, frame)  
             func = function (tpl_args)
 
                -- Format the mod ids for cargo queries:
                 local mlist = {}
                 local mlist = {}
                 for key, _ in pairs(tpl_args._mods) do
                 for _, key in ipairs(tpl_args._mods) do
                     mlist[#mlist+1] = key
                     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
               
                tpl_args._mods = m_cargo.array_query{
                    tables={'mods'},
                    fields={'mods.stat_text'},
                    id_field='mods.id',
                    id_array=mlist
                }
             end,
             end,
         },
         },
Line 374: Line 873:
tables.monster_resistances = {
tables.monster_resistances = {
     table = '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'},
     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 = {
     fields = {
         id = {
         id = {
Line 433: 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', 'summon_life'},
     order = {'level', 'damage', 'evasion', 'accuracy', 'life', 'experience',
            'summon_life'},
     fields = {
     fields = {
         level = {
         level = {
Line 445: Line 948:
         evasion = {
         evasion = {
             field = 'evasion',
             field = 'evasion',
            type = 'Integer',
        },
        armour = {
            field = 'armour',
             type = 'Integer',
             type = 'Integer',
         },
         },
Line 469: 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', 'boss_item_rarity', 'boss_item_quantity'},
     order = {'level', 'life', 'damage', 'boss_life', 'boss_damage',
            'boss_item_rarity', 'boss_item_quantity'},
     fields = {
     fields = {
         level = {
         level = {
Line 527: 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 534: 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 544: 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 551: Line 1,059:
local tbl_view = {
local tbl_view = {
     {
     {
         header = i18n.tooltips.name,
         -- header = i18n.tooltips.name,
         func = display.value{arg='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.experience_multiplier,
         header = i18n.tooltips.area,
         func = display.value{arg='experience_multiplier'},
         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.health_multiplier,
         header = i18n.tooltips.damage,
         func = display.value{arg='health_multiplier'},
         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.damage_multiplier,
         header = i18n.tooltips.aps,
         func = display.value{arg='damage_multiplier'},
         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.attack_speed,
         header = i18n.tooltips.critical_strike_chance_total,
         func = display.value{arg='attack_speed', fmt='%.3fs',},
         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.critical_strike_chance,
         header = i18n.tooltips.armour,
         func = display.value{arg='critical_strike_chance', fmt='%.2f%%',},
         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.minimum_attack_distance,
         header = i18n.tooltips.evasion,
         func = display.value{arg='minimum_attack_distance'},
         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.maximum_attack_distance,
         header = i18n.tooltips.accuracy,
         func = display.value{arg='maximum_attack_distance'},
         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,
         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 593: Line 1,418:
                         :attr('rowspan', 2)
                         :attr('rowspan', 2)
                         :done()
                         :done()
                     :tag('th')
                     -- :tag('th')
                         :wikitext(i18n.tooltips.resistances)
                         -- :wikitext(i18n.tooltips.resistances)
                         :attr('colspan', 4)
                         -- :attr('colspan', 4)
                         :done()
                         -- :done()
                     :done()
                     :done()
                 :tag('tr')
                 :tag('tr')
Line 612: Line 1,437:
                         :done()
                         :done()
                     :done()
                     :done()
                   
 
             for _, k in ipairs({'part1', 'part2', 'maps'}) do
             local difficulties = {'part1', 'part2', 'maps'}
            local elements = {'fire', 'cold', 'lightning', 'chaos'}
            for _, k in ipairs(difficulties) do
                 local tr = tbl:tag('tr')
                 local tr = tbl:tag('tr')
                 tr
                 tr
Line 619: Line 1,446:
                         :wikitext(i18n.tooltips[k])
                         :wikitext(i18n.tooltips[k])
                         :done()
                         :done()
                 for _, element in ipairs({'fire', 'cold', 'lightning', 'chaos'}) do
                 for _, element in ipairs(elements) do
                    local field = string.format(
                        'monster_resistances.%s_%s',
                        k,
                        element
                    )
                     tr
                     tr
                         :tag('td')
                         :tag('td')
                             :attr('class', 'tc -' .. element)
                             :attr('class', 'tc -' .. element)
                             :wikitext(tpl_args.monster_type[string.format('monster_resistances.%s_%s', k, element)])
                             :wikitext(tpl_args.monster_type[field])
                             :done()
                             :done()
                 end
                 end
             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)
             return tostring(tbl)
         end,
         end,
     },
     },
     {
     {
         header = i18n.tooltips.stat_text,
         header = i18n.tooltips.experience,
         func = function (tpl_args, frame)
         func = function(tpl_args)
             if #tpl_args._mods == 0 then
            local f_stats = function(tpl_args, result)
                 return
                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
             end
           
 
             local out = {}
             local f_strings = function(tpl_args, result)
            for _, row in ipairs(tpl_args._mods) do
                 local strings = {
                 out[#out+1] = row['mods.stat_text']
                    -- increased={},
                }
 
                return strings
             end
             end
           
 
             return table.concat(out, '<br>')
             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,
         end,
     },
     },
     {
     {
         header = i18n.tooltips.size,
         header = i18n.tooltips.metadata_id,
         func = display.value{arg='size'},
         func = display.value{arg='metadata_id'},
     },
     },
     {
     {
         header = i18n.tooltips.model_size_multiplier,
         header = i18n.tooltips.monster_type_id,
         func = display.value{arg='model_size_multiplier'},
         func = display.value{arg='monster_type_id'},
     },
     },
     {
     {
         header = i18n.tooltips.tags,
         header = i18n.tooltips.tags,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             if tpl_args.tags == nil or #tpl_args.tags == 0 then
             if tpl_args.tags == nil or #tpl_args.tags == 0 then
                 return
                 return
             end
             end
           
 
           return table.concat(tpl_args.tags, '<br>')
           return table.concat(tpl_args.tags, '<br>')
         end,
         end,
     },
     },
     {
     {
         header = i18n.tooltips.metadata_id,
         header = i18n.tooltips.experience_multiplier,
         func = display.value{arg='metadata_id'},
         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.areas,
         header = i18n.tooltips.size,
         func = function(tpl_args, frame)
         func = display.value{arg='size'},
            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 = 'Life',
         header = i18n.tooltips.model_size_multiplier,
         func = function(tpl_args, frame)
         func = display.value{arg='model_size_multiplier'},
            -- Should this be stored to cargo?
           
            local id = tpl_args.rarity_id or 'normal'
                     
            -- Get monster level from the area level unless it's been
            -- user defined.
            local monster_level = {}
            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
            tpl_args.monster_level = tpl_args.monster_level or table.concat(monster_level, ',')
           
            -- Stop if no monster level was found:
            if tpl_args.monster_level == nil or tpl_args.monster_level == '' then
              return nil
            end
           
            tpl_args.monster_level = m_util.string.split(tpl_args.monster_level, ',')
           
            local results = m_cargo.query(
                {
                    'monster_base_stats',
                    'monster_life_scaling',
                    'monster_map_multipliers',
                    -- 'monster'
                },
                {
                    'monster_base_stats.level',
                    'monster_base_stats.life',
                    'monster_life_scaling.magic',
                    'monster_life_scaling.rare',
                    'monster_map_multipliers.life',
                    '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, ', ')
                    ),
                }
            )
           
            -- Add monster modifiers. Min-max range necessary? item2
            -- has a smarter way to set mods and stats?
            local results2 = m_cargo.query(
                {
                    'mods',
                    'mod_stats',
                },
                {
                    'mods.domain',
                    'mods.generation_type',
                    'mods.mod_group',
                    'mods.id',
                    'mod_stats.id',
                    '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"',
                        id
                    ),
                }
            )
            local mods = results2[1] or {}
           
            local out = {}
            for i, result in ipairs(results) do
           
                local stats = {
                    added={
                        result['monster_base_stats.life'],
                    }
                    ,
                    increased={
                        result['monster_life_scaling.' .. tostring(tpl_args.rarity_id)] or 0,
                    },
                    more={
                        mods['mod_stats.max'] or 0,
                    },
                    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,
                }
                local life = h.stat_calc(stats)
                local life_verb = h.stat_calc_verbose(stats)
               
                out[i] = m_util.html.abbr(
                    string.format('%0.0f', life),
                    string.format(
                        'Lvl. %s: %s',
                        result['monster_base_stats.level'],
                        life_verb
                    )
                )
                end
           
            return table.concat(out, ', ')
        end,
     },
     },
}
}
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 819: Line 1,622:
         metadata_id='Metadata/Monsters/Bandits/BanditBossHeavyStrike_',
         metadata_id='Metadata/Monsters/Bandits/BanditBossHeavyStrike_',
         monster_type_id='BanditBoss',
         monster_type_id='BanditBoss',
         tags='red_blood',  
        mod_ids='MonsterAttackBlock30Bypass20, MonsterExileLifeInMerciless_',
         tags='red_blood',
         skill_ids='Melee, MonsterHeavyStrike',
         skill_ids='Melee, MonsterHeavyStrike',
         name='Calaf, Headstaver',
         name='Calaf, Headstaver',
Line 831: 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
     -- 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,
     }
     }
      
 
     local tbl = mw.html.create('table')
     -- Attach to table
    tbl
     mw.getCurrentFrame():expandTemplate{title = 'Template:Monster/cargo/monsters/attach'}
        :attr('class', 'wikitable')
 
       
     -- Create the infoboxes:
     for _, data in ipairs(tbl_view) do
    local out = {
        local v = data.func(tpl_args, frame)
        h.info_box(tpl_args, tbl_view),
          
         h.info_box(tpl_args, tbl_view_detailed),
        if v ~= nil and v ~= '' then
        h.stat_box(tpl_args),
            tbl
        h.intro_text(tpl_args),
                :tag('tr')
    }
                    :tag('th')
    for _, data in ipairs(list_view) do
                        :wikitext(data.header)
        out[#out+1] = data.func(tpl_args)
                        :done()
                    :tag('td')
                        :wikitext(v)
                        :done()
                    :done()
        end
     end
     end
      
 
     out = {tostring(tbl)}
     -- Categories:
     for _, data in ipairs(list_view) do
     local cats = {
         out[#out+1] = data.func(tpl_args, frame)
        i18n.cats.data,
    }
     local cats_type
    if tpl_args.is_boss then
         cats_type = i18n.cats.boss
    else
        cats_type = tpl_args.rarity
     end
     end
      
     cats[#cats+1] = string.format('%s %s', cats_type, string.lower(i18n.cats.data))
     return table.concat(out)
 
     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