Module:Monster: Difference between revisions

From Path of Exile 2 Wiki
Jump to navigation Jump to search
>Illviljan
m (Handle empty monster type results that can happen when cargo hasn't stored the monster types before the new monster has been created.)
(Support for sandbox and add config subpage)
 
(6 intermediate revisions by 4 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 f_skill_link = require('Module:Skill link').skill_link


local p = {}
-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Monster')


-- ----------------------------------------------------------------------------
local m_game = use_sandbox and mw.loadData('Module:Game/sandbox') or mw.loadData('Module:Game')
-- Strings
-- ----------------------------------------------------------------------------


local i18n = {
local f_skill_link = require('Module:Skill link').skill_link
    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',


    },
-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = use_sandbox and mw.loadData('Module:Monster/config/sandbox') or mw.loadData('Module:Monster/config')


    intro = {
local i18n = cfg.i18n
        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".',
    },
}
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- 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.
         Add mod ids in an ordered way.
Line 295: Line 249:
end
end


function h.stat_calc_per_monster_level(tpl_args, frame, f_stats, f_strings)
function h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
     --[[
     --[[
     Calculate the total stat value per monster level.
     Calculate the total stat value per monster level.
Line 317: Line 271:
     for i, result in ipairs(tpl_args._mod_data['monster_level']) do
     for i, result in ipairs(tpl_args._mod_data['monster_level']) do
         -- Initial stats for monsters:
         -- Initial stats for monsters:
         local stats = f_stats(tpl_args, frame, result)
         local stats = f_stats(tpl_args, result)


         -- Append matching stats from the modifiers:
         -- Append matching stats from the modifiers:
Line 325: Line 279:
                 h.stat_match(
                 h.stat_match(
                     stats,
                     stats,
                     f_strings(tpl_args, frame, result),
                     f_strings(tpl_args, result),
                     v
                     v
                 )
                 )
Line 343: Line 297:




function h.intro_text(tpl_args, frame)
function h.intro_text(tpl_args)
     --[[
     --[[
     Display an introductory text about the monster data.
     Display an introductory text about the monster data.
Line 349: Line 303:
     local out = {}
     local out = {}
     if mw.ustring.find(tpl_args['metadata_id'], '_') then
     if mw.ustring.find(tpl_args['metadata_id'], '_') then
         out[#out+1] = frame:expandTemplate{
         out[#out+1] = mw.getCurrentFrame():expandTemplate{
             title='Incorrect title',
             title='Incorrect title',
             args = {title=tpl_args['id']}
             args = {title=tpl_args['id']}
Line 372: Line 326:
end
end


function h.info_box(tpl_args, frame, tbl_view)
function h.info_box(tpl_args, tbl_view)
     -- Create the infobox:
     -- Create the infobox:
     local container = mw.html.create('div')
     local container = mw.html.create('div')
Line 384: Line 338:


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


         if v ~= nil and v ~= '' then
         if v ~= nil and v ~= '' then
Line 400: Line 354:
end
end


function h.stat_box(tpl_args, frame)
function h.stat_box(tpl_args)
     --[[
     --[[
     Display the stat box.
     Display the stat box.
Line 509: Line 463:
             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', 'items'},
                     {'areas', 'maps', 'items'},
Line 554: Line 508:
             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 672: Line 626:
             field = 'is_boss',
             field = 'is_boss',
             type = 'Boolean',
             type = 'Boolean',
             func = function (tpl_args, frame)
             func = function (tpl_args)
                 -- If the monster is used in some area it's most likely
                 -- If the monster is used in some area it's most likely
                 -- an unique boss:
                 -- an unique boss:
Line 686: Line 640:
             field = 'rarity_id',
             field = 'rarity_id',
             type = 'String',
             type = 'String',
             func = function (tpl_args, frame)
             func = function (tpl_args)
                 --[[
                 --[[
                     Define the rarity of the monster. There's no obvious
                     Define the rarity of the monster. There's no obvious
Line 744: Line 698:
             field = 'rarity',
             field = 'rarity',
             type = 'String',
             type = 'String',
             func = function (tpl_args, frame)
             func = function (tpl_args)


                 local results = m_cargo.map_results_to_id{
                 local results = m_cargo.map_results_to_id{
Line 755: Line 709:
                             'mods.domain',
                             'mods.domain',
                             'mods.generation_type',
                             'mods.generation_type',
                             'mods.mod_group',
                             'mods.mod_groups',
                             'mods.id',
                             'mods.id',
                             'mod_stats.id',
                             'mod_stats.id',
Line 777: Line 731:
                 }
                 }
                 for modid, mod in pairs(results) do
                 for modid, mod in pairs(results) do
                     h.add_mod_id(tpl_args, frame, modid)
                     h.add_mod_id(tpl_args, modid)
                     tpl_args._mod_data[modid] = mod
                     tpl_args._mod_data[modid] = mod
                 end
                 end
Line 789: Line 743:
         --
         --
         mods = {
         mods = {
             func = function (tpl_args, frame)
             func = function (tpl_args)


                 -- Format the mod ids for cargo queries:
                 -- Format the mod ids for cargo queries:
Line 1,031: Line 985:
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 1,048: Line 1,002:


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 1,056: Line 1,010:
     {
     {
         -- header = i18n.tooltips.name,
         -- header = i18n.tooltips.name,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             if tpl_args.name == nil then
             if tpl_args.name == nil then
                 return
                 return
Line 1,071: Line 1,025:
     {
     {
         -- header = i18n.tooltips.image,
         -- header = i18n.tooltips.image,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             local image_name = tpl_args.name or tpl_args.monster_type_id
             local image_name = tpl_args.name or tpl_args.monster_type_id
             image_name = string.gsub(image_name, '[%[%]]', '')
             image_name = string.gsub(image_name, '[%[%]]', '')
            local title = mw.title.makeTitle('File', image_name .. ' monster screenshot.jpg')
        if not (title and title.file and title.file.exists) then
        return
        end
             return string.format(
             return string.format(
                 '[[File:%s monster screenshot.jpg|296x500px]]',
                 '[[File:%s monster screenshot.jpg|296x500px]]',
Line 1,086: Line 1,044:
     {
     {
         header = i18n.tooltips.area,
         header = i18n.tooltips.area,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             local out = {}
             local out = {}
             for i, v in ipairs(tpl_args.monster_usages) do
             for i, v in ipairs(tpl_args.monster_usages) do
Line 1,101: Line 1,059:
     {
     {
         header = i18n.tooltips.monster_level,
         header = i18n.tooltips.monster_level,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             -- Get monster level from the area level unless it's been
             -- Get monster level from the area level unless it's been
             -- user defined.
             -- user defined.
Line 1,162: Line 1,120:
     {
     {
         header = i18n.tooltips.stat_text,
         header = i18n.tooltips.stat_text,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             local out = {}
             local out = {}
             for _, modid in ipairs(tpl_args._mods) do
             for _, modid in ipairs(tpl_args._mods) do
Line 1,184: Line 1,142:
     {
     {
         header = i18n.tooltips.skills,
         header = i18n.tooltips.skills,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             local out = {}
             local out = {}
             for _, id in ipairs(tpl_args.skill_ids or {}) do
             for _, id in ipairs(tpl_args.skill_ids or {}) do
Line 1,198: Line 1,156:
     {
     {
         header = i18n.tooltips.life,
         header = i18n.tooltips.life,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             local f_stats = function(tpl_args, frame, result)
             local f_stats = function(tpl_args, result)
                 local stats = {
                 local stats = {
                     added={
                     added={
Line 1,217: Line 1,175:
             end
             end


             local f_strings = function(tpl_args, frame, result)
             local f_strings = function(tpl_args, result)
                 local strings = {
                 local strings = {
                     added={'base_maximum_life'},
                     added={'base_maximum_life'},
Line 1,233: Line 1,191:
             end
             end


             return h.stat_calc_per_monster_level(tpl_args, frame, f_stats, f_strings)
             return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)


         end,
         end,
Line 1,239: Line 1,197:
     {
     {
         header = i18n.tooltips.damage,
         header = i18n.tooltips.damage,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             local f_stats = function(tpl_args, frame, result)
             local f_stats = function(tpl_args, result)
                 local stats = {
                 local stats = {
                     added={
                     added={
Line 1,256: Line 1,214:
             end
             end


             local f_strings = function(tpl_args, frame, result)
             local f_strings = function(tpl_args, result)
                 local strings = {
                 local strings = {
                     -- added={},
                     -- added={},
Line 1,273: Line 1,231:
             end
             end


             return h.stat_calc_per_monster_level(tpl_args, frame, f_stats, f_strings)
             return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
         end,
         end,
     },
     },
     {
     {
         header = i18n.tooltips.aps,
         header = i18n.tooltips.aps,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             local f_stats = function(tpl_args, frame, result)
             local f_stats = function(tpl_args, result)
                 local stats = {
                 local stats = {
                     added={
                     added={
Line 1,288: Line 1,246:
             end
             end


             local f_strings = function(tpl_args, frame, result)
             local f_strings = function(tpl_args, result)
                 local strings = {
                 local strings = {
                     increased={'map_monsters_attack_speed_+%'}, -- map_monsters_cast_speed_+%
                     increased={'map_monsters_attack_speed_+%'}, -- map_monsters_cast_speed_+%
Line 1,303: Line 1,261:
             end
             end


             return h.stat_calc_per_monster_level(tpl_args, frame, f_stats, f_strings)
             return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
         end,
         end,
     },
     },
     {
     {
         header = i18n.tooltips.critical_strike_chance_total,
         header = i18n.tooltips.critical_strike_chance_total,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             local f_stats = function(tpl_args, frame, result)
             local f_stats = function(tpl_args, result)
                 local stats = {
                 local stats = {
                     added={
                     added={
Line 1,318: Line 1,276:
             end
             end


             local f_strings = function(tpl_args, frame, result)
             local f_strings = function(tpl_args, result)
                 local strings = {
                 local strings = {
                     increased={'map_monsters_critical_strike_chance_+%'},
                     increased={'map_monsters_critical_strike_chance_+%'},
Line 1,326: Line 1,284:
             end
             end


             return h.stat_calc_per_monster_level(tpl_args, frame, f_stats, f_strings)
             return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
         end,
         end,
     },
     },
     {
     {
         header = i18n.tooltips.armour,
         header = i18n.tooltips.armour,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             local f_stats = function(tpl_args, frame, result)
             local f_stats = function(tpl_args, result)
                 local stats = {
                 local stats = {
                     added={
                     added={
Line 1,341: Line 1,299:
             end
             end


             local f_strings = function(tpl_args, frame, result)
             local f_strings = function(tpl_args, result)
                 local strings = {
                 local strings = {
                     -- more={},
                     -- more={},
Line 1,349: Line 1,307:
             end
             end


             return h.stat_calc_per_monster_level(tpl_args, frame, f_stats, f_strings)
             return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
         end,
         end,
     },
     },
     {
     {
         header = i18n.tooltips.evasion,
         header = i18n.tooltips.evasion,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             local f_stats = function(tpl_args, frame, result)
             local f_stats = function(tpl_args, result)
                 local stats = {
                 local stats = {
                     added={
                     added={
Line 1,364: Line 1,322:
             end
             end


             local f_strings = function(tpl_args, frame, result)
             local f_strings = function(tpl_args, result)
                 local strings = {
                 local strings = {
                     -- more={},
                     -- more={},
Line 1,372: Line 1,330:
             end
             end


             return h.stat_calc_per_monster_level(tpl_args, frame, f_stats, f_strings)
             return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
         end,
         end,
     },
     },
     {
     {
         header = i18n.tooltips.accuracy,
         header = i18n.tooltips.accuracy,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             local f_stats = function(tpl_args, frame, result)
             local f_stats = function(tpl_args, result)
                 local stats = {
                 local stats = {
                     added={
                     added={
Line 1,387: Line 1,345:
             end
             end


             local f_strings = function(tpl_args, frame, result)
             local f_strings = function(tpl_args, result)
                 local strings = {
                 local strings = {
                     increased = {'map_monsters_accuracy_rating_+%'},
                     increased = {'map_monsters_accuracy_rating_+%'},
Line 1,396: Line 1,354:
             end
             end


             return h.stat_calc_per_monster_level(tpl_args, frame, f_stats, f_strings)
             return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
         end,
         end,
     },
     },
     {
     {
         header = i18n.tooltips.resistances,
         header = i18n.tooltips.resistances,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             local tbl = mw.html.create('table')
             local tbl = mw.html.create('table')
             tbl
             tbl
Line 1,479: Line 1,437:
     {
     {
         header = i18n.tooltips.experience,
         header = i18n.tooltips.experience,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             local f_stats = function(tpl_args, frame, result)
             local f_stats = function(tpl_args, result)
                 local stats = {
                 local stats = {
                     added={
                     added={
Line 1,490: Line 1,448:
             end
             end


             local f_strings = function(tpl_args, frame, result)
             local f_strings = function(tpl_args, result)
                 local strings = {
                 local strings = {
                     increased={'monster_slain_experience_+%'},
                     increased={'monster_slain_experience_+%'},
Line 1,498: Line 1,456:
             end
             end


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


             local f_stats = function(tpl_args, frame, result)
             local f_stats = function(tpl_args, result)
                 local stats = {
                 local stats = {
                     added={
                     added={
Line 1,519: Line 1,477:
             end
             end


             local f_strings = function(tpl_args, frame, result)
             local f_strings = function(tpl_args, result)
                 local strings = {
                 local strings = {
                     -- increased={},
                     -- increased={},
Line 1,527: Line 1,485:
             end
             end


             return h.stat_calc_per_monster_level(tpl_args, frame, f_stats, f_strings)
             return h.stat_calc_per_monster_level(tpl_args, f_stats, f_strings)
         end,
         end,
     },
     },
Line 1,539: Line 1,497:
local tbl_view_detailed = {
local tbl_view_detailed = {
     {
     {
         func = function(tpl_args, frame)
         func = function(tpl_args)
             return i18n.tooltips.monster_data
             return i18n.tooltips.monster_data
         end,
         end,
Line 1,553: Line 1,511:
     {
     {
         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
Line 1,600: Line 1,558:
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.
Line 1,660: Line 1,608:


     ]]
     ]]
    -- Get args
    tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)


     tpl_args._mods = {}
     tpl_args._mods = {}


     -- Parse and store the monster table:
     -- Parse and store the monster table:
     m_cargo.parse_field_arguments{
     m_util.args.from_cargo_map{
         tpl_args=tpl_args,
         tpl_args=tpl_args,
        frame=frame,
         table_map=tables.monsters,
         table_map=tables.monsters,
     }
     }
    -- Attach to table
    mw.getCurrentFrame():expandTemplate{title = 'Template:Monster/cargo/monsters/attach'}


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


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


     return table.concat(out) .. m_util.misc.add_category(cats)
     return table.concat(out) .. m_util.misc.add_category(cats)
end
end
-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------
local p = {}
p.table_monsters = m_cargo.declare_factory{data=tables.monsters}
p.table_monster_types = m_cargo.declare_factory{data=tables.monster_types}
p.table_monster_resistances = m_cargo.declare_factory{data=tables.monster_resistances}
p.table_monster_base_stats = m_cargo.declare_factory{data=tables.monster_base_stats}
p.table_monster_map_multipliers = m_cargo.declare_factory{data=tables.monster_map_multipliers}
p.table_monster_life_scaling = m_cargo.declare_factory{data=tables.monster_life_scaling}
p.store_data = m_cargo.store_from_lua{tables=tables, module='Monster'}
--
-- Template:Monster
--
p.monster = m_util.misc.invoker_factory(_monster, {
    wrappers = cfg.wrappers.monster,
})


return p
return p

Latest revision as of 17:28, 24 September 2025

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

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

-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Monster')

local m_game = use_sandbox and mw.loadData('Module:Game/sandbox') or mw.loadData('Module:Game')

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

-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = use_sandbox and mw.loadData('Module:Monster/config/sandbox') or mw.loadData('Module:Monster/config')

local i18n = cfg.i18n

-- ----------------------------------------------------------------------------
-- 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.categories.data,
    }
    local cats_type
    if tpl_args.is_boss then
        cats_type = i18n.categories.boss
    else
        cats_type = tpl_args.rarity
    end
    cats[#cats+1] = string.format('%s %s', cats_type, string.lower(i18n.categories.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 = cfg.wrappers.monster,
})

return p