Module:Monster: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
>Illviljan
m (#tpl_args.monster_level should be 0 when no monster levels have been found.)
>Illviljan
(Added a total damage column, moved stuff around because it's similar to the total life calculation.)
Line 21: Line 21:
         name = 'Name',
         name = 'Name',
         rarity = 'Rarity',
         rarity = 'Rarity',
         experience_multiplier = 'Base Experience Multiplier',  
         experience_multiplier = 'Base Experience Multiplier',
         health_multiplier = 'Base Health Multiplier',
         health_multiplier = 'Base Health Multiplier',
         damage_multiplier = 'Base Damage Multiplier',
         damage_multiplier = 'Base Damage Multiplier',
Line 44: Line 44:
         monster_type_id = 'Monster type id',
         monster_type_id = 'Monster type id',
         areas = 'Areas',
         areas = 'Areas',
         monster_level = 'Level'
         monster_level = 'Level',
        skills = 'Skills',
        life = 'Life',
        damage = 'Damage',
     },
     },
   
 
     intro = {
     intro = {
         text_with_name = "'''%s''' is the internal id for the [[%s|%s]] [[monster]]. ",
         text_with_name = "'''%s''' is the internal id for the [[%s|%s]] [[monster]]. ",
         text_without_name = "'''%s''' is the internal id of an unnamed [[monster]]. ",
         text_without_name = "'''%s''' is the internal id of an unnamed [[monster]]. ",
     },
     },
   
 
     errors = {
     errors = {
         invalid_rarity_id = 'The rarity id "%s" is invalid. Acceptable values are "normal", "magic", "rare" and "unique".',
         invalid_rarity_id = 'The rarity id "%s" is invalid. Acceptable values are "normal", "magic", "rare" and "unique".',
Line 62: Line 65:


function h.add_mod_id(tpl_args, frame, value)
function h.add_mod_id(tpl_args, frame, value)
    --[[
        Add mod ids in an ordered way.
    ]]
    if type(value) ~= 'table' then
        value = {value}
    end
     for _, id in ipairs(value or {}) do
     for _, id in ipairs(value or {}) do
         if tpl_args._mods[id] == nil then
         if tpl_args._mods[id] == nil then
Line 68: Line 79:
         end
         end
     end
     end
   
 
     return value
     return value
end
end
Line 75: Line 86:
     --[[
     --[[
     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 90: Line 100:
     }
     }
     = 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 105: Line 115:
         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 116: Line 126:
         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 122: Line 132:
             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 135: Line 159:
         end
         end
     end
     end
   
 
     return out
     return out
end
end
Line 144: Line 168:
     ]]
     ]]
     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 175: Line 206:
         end
         end
     end
     end
   
 
     return table.concat(out, ' * ')
     return table.concat(out, ' * ')
end
end
Line 182: Line 213:
     --[[
     --[[
     Match stats to strings.
     Match stats to strings.
   
 
     Examples
     Examples
     --------
     --------
Line 202: Line 233:
     mw.logObject(stats)
     mw.logObject(stats)
     ]]
     ]]
   
 
     for k, stat_ids in pairs(strings) do
     for k, stat_ids in pairs(strings) do
         for _, stat_id in ipairs(stat_ids) do
         for _, stat_id in ipairs(stat_ids) do
             -- Match the stat. TODO: Is there a smarter way? Can't just find  
             -- Match the stat. TODO: Is there a smarter way? Can't just find
             -- the pattern within the string though since increased and more
             -- the pattern within the string though since increased and more
             -- are so similar.
             -- are so similar.
             if stat_id == result['mod_stats.id'] then  
             if stat_id == result['mod_stats.id'] then
                if stats[k] == nil then
                    stats[k] = {}
                end
                 stats[k][#stats[k]+1] = result['mod_stats.max'] -- TODO: add range.
                 stats[k][#stats[k]+1] = result['mod_stats.max'] -- TODO: add range.
             end  
             end
         end
         end
     end
     end
Line 222: Line 256:
     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] = frame:expandTemplate{
             title='Incorrect title',  
             title='Incorrect title',
             args = {title=tpl_args['id']}  
             args = {title=tpl_args['id']}
         }
         }
     end
     end
   
 
     if tpl_args['name'] then
     if tpl_args['name'] then
         out[#out+1] = string.format(
         out[#out+1] = string.format(
             i18n.intro.text_with_name,  
             i18n.intro.text_with_name,
             tpl_args['metadata_id'],  
             tpl_args['metadata_id'],
             tpl_args['main_page'] or tostring(mw.title.getCurrentTitle()),  
             tpl_args['main_page'] or tostring(mw.title.getCurrentTitle()),
             tpl_args['name']
             tpl_args['name']
         )
         )
     else  
     else
         out[#out+1] = string.format(
         out[#out+1] = string.format(
             i18n.intro.text_without_name,
             i18n.intro.text_without_name,
             tpl_args['metadata_id']
             tpl_args['metadata_id']
         )
         )
     end  
     end
   
 
     return table.concat(out)
     return table.concat(out)
end
end
Line 260: Line 294:
                 tpl_args.monster_usages = m_cargo.query(
                 tpl_args.monster_usages = m_cargo.query(
                     {'areas', 'maps'},
                     {'areas', 'maps'},
                     {            
                     {
                         'areas.name',
                         'areas.name',
                         'areas.id',
                         'areas.id',
Line 271: Line 305:
                     {
                     {
                         join='areas.id=maps.area_id',
                         join='areas.id=maps.area_id',
                         where=string.format('areas.boss_monster_ids HOLDS "%s"', value),  
                         where=string.format('areas.boss_monster_ids HOLDS "%s"', value),
                     }
                     }
                 )
                 )
               
 
                 return value
                 return value
             end,
             end,
Line 290: Line 324:
                         '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]
               
 
                 if tpl_args.monster_type['monster_types.tags'] then
                 if tpl_args.monster_type['monster_types.tags'] then
                     local tags = m_util.string.split(
                     local tags = m_util.string.split(
                         tpl_args.monster_type['monster_types.tags'],  
                         tpl_args.monster_type['monster_types.tags'],
                         ',%s+'
                         ',%s+'
                     )
                     )
                     -- TODO: Maybe this can be fixed earlier?
                     -- TODO: Maybe this can be fixed earlier?
                     if tpl_args.tags == nil then  
                     if tpl_args.tags == nil then
                         tpl_args.tags = {}
                         tpl_args.tags = {}
                     end  
                     end
                     for _, tag in ipairs(tags) do
                     for _, tag in ipairs(tags) do
                         tpl_args.tags[#tpl_args.tags+1] = tag
                         tpl_args.tags[#tpl_args.tags+1] = tag
                     end
                     end
                 end
                 end
               
 
                 return value
                 return value
             end,
             end,
Line 415: Line 449:
             func = function (tpl_args, frame)
             func = function (tpl_args, frame)
                 --[[
                 --[[
                     Define the rarity of the monster. There's no obvious  
                     Define the rarity of the monster. There's no obvious
                     parameter that can be datamined for this so this will  
                     parameter that can be datamined for this so this will
                     be mostly guess work.
                     be mostly guess work.
                 ]]
                 ]]
               
 
                 -- User defined rarity takes priority:
                 -- User defined rarity takes priority:
                 if tpl_args.rarity_id ~= nil then  
                 if tpl_args.rarity_id ~= nil then
                     if m_game.constants.rarities[tpl_args.rarity_id] == nil then
                     if m_game.constants.rarities[tpl_args.rarity_id] == nil then
                         error(string.format(i18n.errors.invalid_rarity_id,  
                         error(string.format(i18n.errors.invalid_rarity_id,
                                             tostring(tpl_args.rarity_id)))
                                             tostring(tpl_args.rarity_id)))
                     end
                     end
                     return tpl_args.rarity_id
                     return tpl_args.rarity_id
                 end
                 end
           
 
                 -- If the monster is used in some area it's most likely
                 -- If the monster is used in some area it's most likely
                 -- an unique boss:
                 -- an unique boss:
Line 435: Line 469:
                     return tpl_args.rarity_id
                     return tpl_args.rarity_id
                 end
                 end
               
 
                 -- If there are no mods it's probably a normal monster:
                 -- If there are no mods it's probably a normal monster:
                 if #tpl_args._mods == 0 then
                 if #tpl_args._mods == 0 then
                     tpl_args.rarity_id = 'normal'
                     tpl_args.rarity_id = 'normal'
                     return tpl_args.rarity_id
                     return tpl_args.rarity_id
                 end  
                 end
               
 
                 -- Try to determine rarity from mods:
                 -- Try to determine rarity from mods:
                 for _, modid in ipairs(tpl_args._mods) do
                 for _, modid in ipairs(tpl_args._mods) do
                     local mod = tpl_args._mod_data[modid]
                     local mod = tpl_args._mod_data[modid]
                     -- Check if the mod contains the monster rarity stat:  
                     -- Check if the mod contains the monster rarity stat:
                     for _, v in ipairs(mod) do  
                     for _, v in ipairs(mod) do
                         if v['mod_stats.id'] == 'monster_rarity' then
                         if v['mod_stats.id'] == 'monster_rarity' then
                             -- TODO: m_game rarity id does not match the stat:
                             -- TODO: m_game rarity id does not match the stat:
                             local int_id = tonumber(v['mod_stats.max']) + 1  
                             local int_id = tonumber(v['mod_stats.max']) + 1
                             for k, row in pairs(m_game.constants.rarities) do
                             for k, row in pairs(m_game.constants.rarities) do
                                 if int_id == row['id'] then  
                                 if int_id == row['id'] then
                                     tpl_args.rarity_id = k
                                     tpl_args.rarity_id = k
                                     return tpl_args.rarity_id
                                     return tpl_args.rarity_id
Line 459: Line 493:
                     end
                     end
                 end
                 end
               
 
                 -- If none of the mods contains the monster rarity  
                 -- If none of the mods contains the monster rarity
                 -- stat then it might be an unique:
                 -- stat then it might be an unique:
                 if tpl_args.rarity_id == nil then  
                 if tpl_args.rarity_id == nil then
                     tpl_args.rarity_id = 'unique'
                     tpl_args.rarity_id = 'unique'
                     return tpl_args.rarity_id
                     return tpl_args.rarity_id
Line 472: Line 506:
             type = 'String',
             type = 'String',
             func = function (tpl_args, frame)
             func = function (tpl_args, frame)
                return m_game.constants.rarities[tpl_args.rarity_id]['full']
 
            end
                 local results = m_cargo.map_results_to_id{
        },
   
        --
        -- Processing fields
        --
        mods = {
            func = function (tpl_args, frame)
               
                -- Format the mod ids for cargo queries:
                 local mlist = {}
                for _, key in ipairs(tpl_args._mods) do
                    mlist[#mlist+1] = string.format('"%s"', key)
                end
                if #mlist == 0 then
                    return
                end
               
                -- tpl_args._mods = m_cargo.array_query{
                    -- tables={'mods', 'mod_stats'},
                    -- fields={'mods.stat_text', 'mods.generation_type',
                    --        'mod_stats.id', 'mod_stats.max'},
                    -- id_field='mods.id',
                    -- query={join='mods._pageID=mod_stats._pageID'},
                    -- id_array=mlist,
                -- }
               
                tpl_args._mod_data = m_cargo.map_results_to_id{
                     results=m_cargo.query(
                     results=m_cargo.query(
                         {
                         {
                             'mods',  
                             'mods',
                             'mod_stats',
                             'mod_stats',
                         },
                         },
                         {
                         {
                            'mods.domain',
                            'mods.generation_type',
                            'mods.mod_group',
                             'mods.id',
                             'mods.id',
                            'mods.stat_text',
                            'mods.generation_type',
                             'mod_stats.id',
                             'mod_stats.id',
                             'mod_stats.max',
                             'mod_stats.max',
                            'mod_stats.min',
                         },
                         },
                         {
                         {
                             join=[[
                             join='mods._pageID=mod_stats._pageID',
                                mods._pageID=mod_stats._pageID
                            ]],
                             where=string.format([[
                             where=string.format([[
                                 mods.id IN (%s)
                                    mods.domain = 3
                            ]], table.concat(mlist, ',')),
                                AND mods.generation_type = 3
                                 AND mods.id REGEXP "Monster%s[0-9]*$"
                                ]],
                                tpl_args.rarity_id
                            ),
                         }
                         }
                     ),
                     ),
Line 525: Line 536:
                     keep_id_field=false,
                     keep_id_field=false,
                 }
                 }
                for modid, mod in pairs(results) do
                    h.add_mod_id(tpl_args, frame, 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, frame)
                -- Format the mod ids for cargo queries:
                local mlist = {}
                for _, key in ipairs(tpl_args._mods) do
                    mlist[#mlist+1] = string.format('"%s"', key)
                end
               
                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.max',
                            },
                            {
                                join=[[
                                    mods._pageID=mod_stats._pageID
                                ]],
                                where=string.format([[
                                    mods.id IN (%s)
                                ]], table.concat(mlist, ',')),
                            }
                        ),
                        field='mods.id',
                        keep_id_field=false,
                    }
                end
             end,
             end,
         },
         },
Line 567: Line 627:
tables.monster_resistances = {
tables.monster_resistances = {
     table = 'monster_resistances',
     table = 'monster_resistances',
     order = {'id', 'part1_fire', 'part1_cold', 'part1_lightning',  
     order = {'id', 'part1_fire', 'part1_cold', 'part1_lightning',
             'part1_chaos', 'part2_fire', 'part2_cold', 'part2_lightning',  
             'part1_chaos', 'part2_fire', 'part2_cold', 'part2_lightning',
             'part2_chaos', 'maps_fire', 'maps_cold', 'maps_lightning',  
             'part2_chaos', 'maps_fire', 'maps_cold', 'maps_lightning',
             'maps_chaos'},
             'maps_chaos'},
     fields = {
     fields = {
Line 629: Line 689:
tables.monster_base_stats = {
tables.monster_base_stats = {
     table = 'monster_base_stats',
     table = 'monster_base_stats',
     order = {'level', 'damage', 'evasion', 'accuracy', 'life', 'experience',  
     order = {'level', 'damage', 'evasion', 'accuracy', 'life', 'experience',
             'summon_life'},
             'summon_life'},
     fields = {
     fields = {
Line 666: Line 726:
tables.monster_map_multipliers = {
tables.monster_map_multipliers = {
     table = 'monster_map_multipliers',
     table = 'monster_map_multipliers',
     order = {'level', 'life', 'damage', 'boss_life', 'boss_damage',  
     order = {'level', 'life', 'damage', 'boss_life', 'boss_damage',
             'boss_item_rarity', 'boss_item_quantity'},
             'boss_item_rarity', 'boss_item_quantity'},
     fields = {
     fields = {
Line 732: Line 792:
             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 751: Line 811:
         -- header = i18n.tooltips.name,
         -- header = i18n.tooltips.name,
         func = function (tpl_args, frame)
         func = function (tpl_args, frame)
             if tpl_args.name == nil then  
             if tpl_args.name == nil then
                 return
                 return
             end
             end
           
 
             local linked_name = string.format(
             local linked_name = string.format(
                 '[[Monster:%s|%s]]',  
                 '[[Monster:%s|%s]]',
                 string.gsub(tpl_args.metadata_id, '_', '~'),  
                 string.gsub(tpl_args.metadata_id, '_', '~'),
                 tpl_args.name
                 tpl_args.name
             )
             )
Line 767: Line 827:
         func = function (tpl_args, frame)
         func = function (tpl_args, frame)
             return string.format(
             return string.format(
                 '[[File:%s monster screenshot.jpg|296x500px]]',  
                 '[[File:%s monster screenshot.jpg|296x500px]]',
                 tpl_args.name or tpl_args.monster_type_id
                 tpl_args.name or tpl_args.monster_type_id
             )
             )
Line 780: Line 840:
         func = function(tpl_args, frame)
         func = function(tpl_args, frame)
             local out = {}
             local out = {}
             for i, v in ipairs(tpl_args.monster_usages) do  
             for i, v in ipairs(tpl_args.monster_usages) do
                 out[#out+1] = string.format(
                 out[#out+1] = string.format(
                     '[[%s|%s]]',  
                     '[[%s|%s]]',
                     v['areas.main_page'] or v['areas._pageName'],  
                     v['areas.main_page'] or v['areas._pageName'],
                     v['areas.name'] or v['areas.id']
                     v['areas.name'] or v['areas.id']
                 )
                 )
             end
             end
           
 
             return table.concat(out, ', ')
             return table.concat(out, ', ')
         end  
         end
     },
     },
     {
     {
         header = i18n.tooltips.monster_level,
         header = i18n.tooltips.monster_level,
         func = function(tpl_args, frame)
         func = function(tpl_args, frame)
             -- 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.
             local monster_level = {}
             local monster_level = {}
             if tpl_args.monster_level then  
             if tpl_args.monster_level then
                 monster_level = m_util.string.split(tpl_args.monster_level, ',')
                 monster_level = m_util.string.split(tpl_args.monster_level, ',')
             else
             else
Line 806: Line 866:
             end
             end
             tpl_args.monster_level = monster_level
             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'
                    },
                    {
                        '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',
                        -- 'monster.health_multiplier',
 
                        -- Damage:
                        'monster_base_stats.damage',
                        'monster_map_multipliers.damage',
                        'monster_map_multipliers.boss_damage',
                    },
                    {
                        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, ', ')
             return table.concat(tpl_args.monster_level, ', ')
         end  
         end
     },
     },
   
     {
     {
         header = i18n.tooltips.stat_text,
         header = i18n.tooltips.stat_text,
Line 819: Line 915:
                 local mod = tpl_args._mod_data[modid] or {}
                 local mod = tpl_args._mod_data[modid] or {}
                 local stat_text = {}
                 local stat_text = {}
               
 
                 -- Add stat_text for each modifier, ignore duplicates:
                 -- Add stat_text for each modifier, ignore duplicates:
                 for _, v in ipairs(mod) do  
                 for _, v in ipairs(mod) do
                     if v['mods.stat_text'] then
                     if v['mods.stat_text'] then
                         if stat_text[v['mods.stat_text']] == nil then
                         if stat_text[v['mods.stat_text']] == nil then
Line 830: Line 926:
                 end
                 end
             end
             end
           
 
             return table.concat(out, '<br>')
             return table.concat(out, '<br>')
         end,
         end,
     },
     },
     {
     {
         header = 'Skills',
         header = i18n.tooltips.skills,
         func = function (tpl_args, frame)
         func = function (tpl_args, frame)
             local out = {}
             local out = {}
             for _, id in ipairs(tpl_args.skill_ids or {}) do
             for _, id in ipairs(tpl_args.skill_ids or {}) do
                 out[#out+1] = f_skill_link{id=id}
                 out[#out+1] = f_skill_link{id=id}
                 if string.find(out[#out], 'class="module%-error"') then  
                 if string.find(out[#out], 'class="module%-error"') then
                     out[#out] = id
                     out[#out] = id
                 end  
                 end
             end
             end
           
 
             return table.concat(out, '<br>')
             return table.concat(out, '<br>')
         end,
         end,
     },
     },
     {
     {
         header = 'Life',
         header = i18n.tooltips.life,
         func = function(tpl_args, frame)
         func = function(tpl_args, frame)
             -- TODO: Should this be stored to cargo?  
             --[[
                       
                TODO: Should this be stored to cargo?
            ]]
 
             -- Stop if no monster level was found:
             -- Stop if no monster level was found:
             if #tpl_args.monster_level == 0 then
             if #tpl_args.monster_level == 0 then
               return nil
               return nil
             end
             end
           
 
             local results = m_cargo.query(
             -- Calculate the total stat value for each monster level:
                {
                    '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? Do item2
            -- have 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"
                        ]],
                        tpl_args.rarity_id
                    ),
                }
            )
            local mods = results2[1] or {}
           
             local out = {}
             local out = {}
             for i, result in ipairs(results) do
             for i, result in ipairs(tpl_args._mod_data['monster_level']) do
                 -- Initial stats for monsters:
                 -- Initial stats for monsters:
                 local stats = {
                 local stats = {
                     added={
                     added={
                         result['monster_base_stats.life'],
                         result['monster_base_stats.life'],
                     }
                     },
                    ,
                     increased={
                     increased={
                         result['monster_life_scaling.' .. tpl_args.rarity_id] or 0,
                         result['monster_life_scaling.' .. tpl_args.rarity_id] or 0,
                     },
                     },
                     more={
                     more={
                         mods['mod_stats.max'] or 0,
                         result['mod_stats.max'] or 0,
                     },
                     },
                     m1 = tpl_args.health_multiplier or 1,
                     m1 = tpl_args.health_multiplier or 1,
                     m2 = (result['monster_map_multipliers.life'] or 100)/100,
                     m_map = (result['monster_map_multipliers.life'] or 100)/100
                    m3 = (result['monster_map_multipliers.boss_life'] or 100)/100,
                 }
                 }
                  
 
                 -- Bossses have different multipliers:
                if tpl_args.is_boss then
                    stats.m_map = (result['monster_map_multipliers.boss_life'] or 100)/100
                end
 
                 -- Append matching stats from the modifiers:
                 -- Append matching stats from the modifiers:
                 for _, modid in ipairs(tpl_args._mods) do
                 for _, modid in ipairs(tpl_args._mods) do
                     local mod = tpl_args._mod_data[modid]
                     local mod = tpl_args._mod_data[modid]
                     for _, v in ipairs(mod) do  
                     for _, v in ipairs(mod) do
                         h.stat_match(
                         h.stat_match(
                             stats,
                             stats,
Line 944: Line 988:
                                 added={'base_maximum_life'},
                                 added={'base_maximum_life'},
                                 increased={'maximum_life_+%'},
                                 increased={'maximum_life_+%'},
                                 more={'maximum_life_+%_final'},
                                 more={
                                    'maximum_life_+%_final',
                                    'monster_life_+%_final_from_rarity'
                                },
                             },
                             },
                             v
                             v
Line 950: Line 997:
                     end
                     end
                 end
                 end
                  
 
                 -- Calculate the total stat value:
                local total_stat_value = h.stat_calc(stats)
                local total_stat_value_verb = h.stat_calc_verbose(stats)
 
                -- Format the output:
                out[i] = m_util.html.abbr(
                    string.format('%0.0f', total_stat_value),
                    string.format(
                        'Lvl. %s: %s',
                        result['monster_base_stats.level'],
                        total_stat_value_verb
                    )
                )
                end
 
            return table.concat(out, ', ')
        end,
    },
    {
        header = i18n.tooltips.damage,
        func = function(tpl_args, frame)
            -- TODO: Should this be stored to cargo?
 
            -- 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 = {
                    added={
                        result['monster_base_stats.damage'],
                    },
                    -- increased={},
                    -- more={},
                    m1 = tpl_args.damage_multiplier or 1,
                    m_map = (result['monster_map_multipliers.damage'] or 100)/100
                }
 
                -- Bossses have different multipliers:
                if tpl_args.is_boss then
                    stats.m_map = (result['monster_map_multipliers.boss_damage'] or 100)/100
                end
 
                -- 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,
                            {
                                -- added={'base_maximum_life'},
                                -- increased={'maximum_life_+%'},
                                more={'monster_rarity_damage_+%_final'},
                                less={'monster_rarity_attack_cast_speed_+%_and_damage_-%_final'},
                            },
                            v
                        )
                    end
                end
 
                 -- Calculate the total stat value:
                 -- Calculate the total stat value:
                 local life = h.stat_calc(stats)
                 local total_stat_value = h.stat_calc(stats)
                 local life_verb = h.stat_calc_verbose(stats)
                 local total_stat_value_verb = h.stat_calc_verbose(stats)
               
 
                 -- Format the output:
                 -- Format the output:
                 out[i] = m_util.html.abbr(
                 out[i] = m_util.html.abbr(
                     string.format('%0.0f', life),  
                     string.format('%0.0f', total_stat_value),
                     string.format(
                     string.format(
                         'Lvl. %s: %s',
                         'Lvl. %s: %s',
                         result['monster_base_stats.level'],
                         result['monster_base_stats.level'],
                         life_verb
                         total_stat_value_verb
                     )
                     )
                 )
                 )
                 end
                 end
           
 
             return table.concat(out, ', ')
             return table.concat(out, ', ')
         end,
         end,
Line 999: Line 1,110:
                         :done()
                         :done()
                     :done()
                     :done()
           
 
             local difficulties = {'part1', 'part2', 'maps'}
             local difficulties = {'part1', 'part2', 'maps'}
             local elements = {'fire', 'cold', 'lightning', 'chaos'}
             local elements = {'fire', 'cold', 'lightning', 'chaos'}
Line 1,010: Line 1,121:
                 for _, element in ipairs(elements) do
                 for _, element in ipairs(elements) do
                     local field = string.format(
                     local field = string.format(
                         'monster_resistances.%s_%s',  
                         'monster_resistances.%s_%s',
                         k,  
                         k,
                         element
                         element
                     )
                     )
Line 1,021: Line 1,132:
                 end
                 end
             end
             end
           
 
             -- -- Compressed resistance table:
             -- -- Compressed resistance table:
             -- local tbl = mw.html.create('table')
             -- local tbl = mw.html.create('table')
Line 1,027: Line 1,138:
             -- local res = {}
             -- local res = {}
             -- for _, element in ipairs(elements) do
             -- for _, element in ipairs(elements) do
                 -- if res[element] == nil then  
                 -- if res[element] == nil then
                     -- res[element] = {}
                     -- res[element] = {}
                 -- end  
                 -- end
                 -- for _, k in ipairs(difficulties) do
                 -- for _, k in ipairs(difficulties) do
                     -- local r = string.format('monster_resistances.%s_%s', k, element)
                     -- local r = string.format('monster_resistances.%s_%s', k, element)
                     -- res[element][#res[element]+1] = m_util.html.abbr(
                     -- res[element][#res[element]+1] = m_util.html.abbr(
                         -- tpl_args.monster_type[r],  
                         -- tpl_args.monster_type[r],
                         -- k
                         -- k
                     -- )
                     -- )
Line 1,043: Line 1,154:
                         -- :done()
                         -- :done()
             -- end
             -- end
           
 
             return tostring(tbl)
             return tostring(tbl)
         end,
         end,
Line 1,089: Line 1,200:
                 return
                 return
             end
             end
           
 
           return table.concat(tpl_args.tags, '<br>')
           return table.concat(tpl_args.tags, '<br>')
         end,
         end,
Line 1,123: Line 1,234:
     --[[
     --[[
     Stores data and display infoboxes of monsters.
     Stores data and display infoboxes of monsters.
   
 
     Example
     Example
     -------
     -------
Line 1,130: Line 1,241:
         monster_type_id='BanditBoss',
         monster_type_id='BanditBoss',
         mod_ids='MonsterAttackBlock30Bypass20, MonsterExileLifeInMerciless_',
         mod_ids='MonsterAttackBlock30Bypass20, MonsterExileLifeInMerciless_',
         tags='red_blood',  
         tags='red_blood',
         skill_ids='Melee, MonsterHeavyStrike',
         skill_ids='Melee, MonsterHeavyStrike',
         name='Calaf, Headstaver',
         name='Calaf, Headstaver',
Line 1,142: Line 1,253:
         critical_strike_chance=5.0,
         critical_strike_chance=5.0,
         attack_speed=1.35,
         attack_speed=1.35,
       
 
         rarity_id = 'unique'
         rarity_id = 'unique'
     }
     }
     ]]
     ]]
   
 
     -- Get args
     -- Get args
     tpl_args = getArgs(frame, {
     tpl_args = getArgs(frame, {
Line 1,152: Line 1,263:
     })
     })
     frame = m_util.misc.get_frame(frame)
     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_cargo.parse_field_arguments{
Line 1,161: Line 1,272:
         table_map=tables.monsters,
         table_map=tables.monsters,
     }
     }
       
 
     -- Create the infobox:
     -- Create the infobox:
     local tbl = mw.html.create('table')
     local tbl = mw.html.create('table')
Line 1,167: Line 1,278:
         :attr('class', 'wikitable')
         :attr('class', 'wikitable')
         :attr('style', 'float:right; margin-left: 10px;')
         :attr('style', 'float:right; margin-left: 10px;')
       
 
     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, frame)
       
 
         if v ~= nil and v ~= '' then
         if v ~= nil and v ~= '' then
             local tr = tbl:tag('tr')
             local tr = tbl:tag('tr')
             if data.header then  
             if data.header then
                 tr:tag('th'):wikitext(data.header):done()
                 tr:tag('th'):wikitext(data.header):done()
                 tr:tag('td'):wikitext(v):done()
                 tr:tag('td'):wikitext(v):done()
             else  
             else
                 tr:tag('th'):attr('colspan', 2):wikitext(v):done()
                 tr:tag('th'):attr('colspan', 2):wikitext(v):done()
             end
             end
         end
         end
     end
     end
   
 
     local out = {
     local out = {
         tostring(tbl),
         tostring(tbl),
Line 1,189: Line 1,300:
         out[#out+1] = data.func(tpl_args, frame)
         out[#out+1] = data.func(tpl_args, frame)
     end
     end
   
 
     local cats = {
     local cats = {
         i18n.cats.data,
         i18n.cats.data,

Revision as of 14:55, 2 November 2019

-- ----------------------------------------------------------------------------
-- Imports
-- ----------------------------------------------------------------------------
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 = {}

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

local i18n = {
    cats = {
        data = 'Monster data'
    },
    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',
        areas = 'Areas',
        monster_level = 'Level',
        skills = 'Skills',
        life = 'Life',
        damage = 'Damage',
    },

    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".',
    },
}
-- ----------------------------------------------------------------------------
-- Helpers
-- ----------------------------------------------------------------------------
local h = {}

function h.add_mod_id(tpl_args, frame, 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 stats to strings.

    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.intro_text(tpl_args, frame)
    --[[
    Display an introductory text about the monster data.
    ]]
    local out = {}
    if mw.ustring.find(tpl_args['metadata_id'], '_') then
        out[#out+1] = frame:expandTemplate{
            title='Incorrect title',
            args = {title=tpl_args['id']}
        }
    end

    if tpl_args['name'] then
        out[#out+1] = string.format(
            i18n.intro.text_with_name,
            tpl_args['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


-- ----------------------------------------------------------------------------
-- 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, frame, value)
                tpl_args.monster_usages = m_cargo.query(
                    {'areas', 'maps'},
                    {
                        'areas.name',
                        'areas.id',
                        'areas.area_level',
                        'areas.boss_monster_ids',
                        'areas.main_page',
                        'areas._pageName',
                        'maps.area_level',
                    },
                    {
                        join='areas.id=maps.area_id',
                        where=string.format('areas.boss_monster_ids HOLDS "%s"', value),
                    }
                )

                return value
            end,
        },
        monster_type_id = {
            field = 'monster_type_id',
            type = 'String',
            func = function (tpl_args, frame, 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]

                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, frame)
                -- 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, frame)
                --[[
                    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, frame)

                local results = m_cargo.map_results_to_id{
                    results=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 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, frame, 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, frame)

                -- Format the mod ids for cargo queries:
                local mlist = {}
                for _, key in ipairs(tpl_args._mods) do
                    mlist[#mlist+1] = string.format('"%s"', key)
                end
                
                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.max',
                            },
                            {
                                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',
        },
        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, frame)
        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, frame)
        return tpl_args[args.sub][args.arg]
    end
end

local tbl_view = {
    {
        -- header = i18n.tooltips.name,
        func = function (tpl_args, frame)
            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, frame)
            return string.format(
                '[[File:%s monster screenshot.jpg|296x500px]]',
                tpl_args.name or tpl_args.monster_type_id
            )
        end,
    },
    -- {
        -- header = i18n.tooltips.rarity,
        -- func = display.value{arg='rarity'},
    -- },
    {
        header = i18n.tooltips.areas,
        func = function(tpl_args, frame)
            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, frame)
            -- 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'
                    },
                    {
                        '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',
                        -- 'monster.health_multiplier',

                        -- Damage:
                        'monster_base_stats.damage',
                        'monster_map_multipliers.damage',
                        'monster_map_multipliers.boss_damage',
                    },
                    {
                        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, frame)
            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, frame)
            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, frame)
            --[[
                TODO: Should this be stored to cargo?
            ]]

            -- 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 = {
                    added={
                        result['monster_base_stats.life'],
                    },
                    increased={
                        result['monster_life_scaling.' .. tpl_args.rarity_id] or 0,
                    },
                    more={
                        result['mod_stats.max'] or 0,
                    },
                    m1 = tpl_args.health_multiplier or 1,
                    m_map = (result['monster_map_multipliers.life'] or 100)/100
                }

                -- Bossses have different multipliers:
                if tpl_args.is_boss then
                    stats.m_map = (result['monster_map_multipliers.boss_life'] or 100)/100
                end

                -- 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,
                            {
                                added={'base_maximum_life'},
                                increased={'maximum_life_+%'},
                                more={
                                    'maximum_life_+%_final',
                                    'monster_life_+%_final_from_rarity'
                                },
                            },
                            v
                        )
                    end
                end

                -- Calculate the total stat value:
                local total_stat_value = h.stat_calc(stats)
                local total_stat_value_verb = h.stat_calc_verbose(stats)

                -- Format the output:
                out[i] = m_util.html.abbr(
                    string.format('%0.0f', total_stat_value),
                    string.format(
                        'Lvl. %s: %s',
                        result['monster_base_stats.level'],
                        total_stat_value_verb
                    )
                )
                end

            return table.concat(out, ', ')
        end,
    },
    {
        header = i18n.tooltips.damage,
        func = function(tpl_args, frame)
            -- TODO: Should this be stored to cargo?

            -- 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 = {
                    added={
                        result['monster_base_stats.damage'],
                    },
                    -- increased={},
                    -- more={},
                    m1 = tpl_args.damage_multiplier or 1,
                    m_map = (result['monster_map_multipliers.damage'] or 100)/100
                }

                -- Bossses have different multipliers:
                if tpl_args.is_boss then
                    stats.m_map = (result['monster_map_multipliers.boss_damage'] or 100)/100
                end

                -- 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,
                            {
                                -- added={'base_maximum_life'},
                                -- increased={'maximum_life_+%'},
                                more={'monster_rarity_damage_+%_final'},
                                less={'monster_rarity_attack_cast_speed_+%_and_damage_-%_final'},
                            },
                            v
                        )
                    end
                end

                -- Calculate the total stat value:
                local total_stat_value = h.stat_calc(stats)
                local total_stat_value_verb = h.stat_calc_verbose(stats)

                -- Format the output:
                out[i] = m_util.html.abbr(
                    string.format('%0.0f', total_stat_value),
                    string.format(
                        'Lvl. %s: %s',
                        result['monster_base_stats.level'],
                        total_stat_value_verb
                    )
                )
                end

            return table.concat(out, ', ')
        end,
    },
    {
        header = i18n.tooltips.resistances,
        func = function (tpl_args, frame)
            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_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',},
    },
    {
        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'},
    },
    {
        header = i18n.tooltips.tags,
        func = function (tpl_args, frame)
            if tpl_args.tags == nil or #tpl_args.tags == 0 then
                return
            end

           return table.concat(tpl_args.tags, '<br>')
        end,
    },
    {
        header = i18n.tooltips.metadata_id,
        func = display.value{arg='metadata_id'},
    },
    {
        header = i18n.tooltips.monster_type_id,
        func = display.value{arg='monster_type_id'},
    },
}

local list_view = {
}


-- ----------------------------------------------------------------------------
-- Page views
-- ----------------------------------------------------------------------------

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

function p.monster(frame)
    --[[
    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'
    }
    ]]

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

    tpl_args._mods = {}

    -- Parse and store the monster table:
    m_cargo.parse_field_arguments{
        tpl_args=tpl_args,
        frame=frame,
        table_map=tables.monsters,
    }

    -- Create the infobox:
    local tbl = mw.html.create('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, frame)

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

    local out = {
        tostring(tbl),
        h.intro_text(tpl_args, frame),
    }
    for _, data in ipairs(list_view) do
        out[#out+1] = data.func(tpl_args, frame)
    end

    local cats = {
        i18n.cats.data,
    }
    return table.concat(out) .. m_util.misc.add_category(cats)
end

return p