Module:Attacks/Tables

From Melvor Idle
< Module:Attacks
Revision as of 21:27, 4 January 2022 by Falterfire (talk | contribs) (Fixed a thing where some special attacks have a scalable number of attacks so total duration shouldn't be shown)
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

Documentation for this module may be created at Module:Attacks/Tables/doc

local p = {}

local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local Monsters = require('Module:Monsters')
local Magic = require('Module:Magic')
local Attacks = require('Module:Attacks')

function p._getSpecialAttackTable(effectDefn, categories, sourceHeaderLabel, includeSource)
    local spAttTable = {}
    local attacks = Attacks.getAttacks(function(attack)
                                           if effectDefn == nil then
                                               return true
                                           else
                                               return Attacks.attackHasEffect(attack, effectDefn)
                                           end
                                       end)
    local includeCat = {}
    for i, category in ipairs(categories) do
        includeCat[category] = true
    end

    -- Compile a list of monsters, items, spells, etc. for each included attack
    for i, spAtt in ipairs(attacks) do
        -- Monsters
        if includeCat['Monster'] then
            for j, monsterID in ipairs(spAtt.monsters) do
                local monster = Monsters.getMonsterByID(monsterID)
                local overrideChance = (monster.overrideSpecialChances ~= nil and Shared.tableCount(monster.overrideSpecialChances) > 0)
                local attChance = spAtt.defaultChance
                if overrideChance then
                    local attIdx = nil
                    for k, monsterAttack in ipairs(monster.specialAttacks) do
                        local attID = (type(monsterAttack) == 'table' and monsterAttack.id) or monsterAttack
                        if spAtt.id == attID then
                            attIdx = k
                            break
                        end
                    end
                    if attIdx ~= nil then
                        attChance = monster.overrideSpecialChances[attIdx]
                    end
                end

                table.insert(spAttTable, { idx = i, source = 'Monster', sourceSort = monster.name, sourceText = Icons.Icon({ monster.name, type = 'monster' }), chance = attChance, descType = 'monster' })
            end
        end

        -- Items/Weapons
        if includeCat['Item'] then
            for j, itemID in ipairs(spAtt.items) do
                local item = Items.getItemByID(itemID)
                table.insert(spAttTable, { idx = i, source = 'Weapon', sourceSort = item.name, sourceText = Icons.Icon({ item.name, type = 'item' }), chance = spAtt.defaultChance, descType = 'player' })
            end
        end

        -- Spells
        if includeCat['Spell'] then
            for j, spellID in ipairs(spAtt.spells) do
                local spell = Magic.getSpellByID(spellID[1], spellID[2])
                table.insert(spAttTable, { idx = i, source = 'Spell', sourceSort = spell.name, sourceText = Icons.Icon({ spell.name, type = 'spell' }), chance = spAtt.defaultChance, descType = 'player' })
            end
        end
    end

    -- Summoning familiars. Any effects inflicted by combat familiars aren't actually special
    -- attacks, therefore the handling here is a bit different and outside of the above attack loop
    if includeCat['Familiar'] then
        local famIdx = Shared.tableCount(attacks) + 1
        local familiars = Items.getItems(function(item)
                                             if item.type == 'Familiar' and Items._getItemStat(item, 'summoningMaxhit') ~= nil and item.modifiers ~= nil then
                                                 local famAttack = { prehitEffects = {}, onhitEffects = { { type = 'Modifier', subtype = 'Familiar', modifiers = item.modifiers } } }
                                                 if effectDefn == nil then
                                                     return Shared.tableCount(Attacks.getAttackEffects(famAttack)) > 0
                                                 else
                                                     return Attacks.attackHasEffect(famAttack, effectDefn)
                                                 end
                                             end
                                             return false
                                         end)
        for j, familiar in ipairs(familiars) do
            -- For chance, assume the first modifier we come across has the chance, which is pretty lazy
            local famChance, famDesc = 0, ''
            for modName, modVal in pairs(familiar.modifiers) do
                if type(modVal) == 'table' and type(modVal[1]) == 'number' then
                    famChance = modVal[1]
                elseif type(modVal) == 'number' then
                    famChance = modVal
                else
                    famChance = 0
                end
                famDesc = Constants._getModifierText(modName, modVal, false)
                break
            end

            table.insert(spAttTable, { idx = famIdx, source = 'Familiar', sourceSort = familiar.name, sourceText = Icons.Icon({ familiar.name, type = 'item' }), chance = famChance or 0, descType = 'player' })
            -- Slap a dummy entry into the attacks table for this familiar
            attacks[famIdx] = { name = familiar.name .. ' (Familiar)', description = { player = famDesc } }
            famIdx = famIdx + 1
        end
    end

    -- Nothing to output if there are no row definitions
    if Shared.tableCount(spAttTable) == 0 then
    	return ''
    end

    -- Sort entries into desired order and generate stats to determine row spans:
    -- By attack index, description type (monster/player), chance, source, then source name (weapon/item/etc.)
    table.sort(spAttTable, function (a, b)
                               local sortKeys = { 'idx', 'descType', 'chance', 'source', 'sourceSort' }
                               for i, key in ipairs(sortKeys) do
                                   if a[key] ~= b[key] then
                                       return a[key] < b[key]
                                   end
                               end
                               return false
                           end)
    -- Determine row counts for grouping/rowspans
    local rowCounts = {}
    for i, rowDefn in ipairs(spAttTable) do
        local idx, dt, chance = rowDefn.idx, rowDefn.descType, rowDefn.chance
        if rowCounts[idx] == nil then
            rowCounts[idx] = { rows = 0 }
        end
        if rowCounts[idx][dt] == nil then
            rowCounts[idx][dt] = { rows = 0 }
        end
        if rowCounts[idx][dt][chance] == nil then
            rowCounts[idx][dt][chance] = 0
        end
        rowCounts[idx]['rows'] = rowCounts[idx]['rows'] + 1
        rowCounts[idx][dt]['rows'] = rowCounts[idx][dt]['rows'] + 1
        rowCounts[idx][dt][chance] = rowCounts[idx][dt][chance] + 1
    end

    -- Generate output table
    local resultPart = {}
    table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
    table.insert(resultPart, '\r\n|- class="headerRow-0"')
    table.insert(resultPart, '\r\n!Name!!style="min-width:225px"| ' .. sourceHeaderLabel .. (includeSource and '!!Type' or '') .. '!!Chance!!Effect')

    local firstRow = { idx = true, descType = true, chance = true }
    local prevRowVal = { idx = 0, descType = '', chance = 0 }
    local resetOnChange = {
        idx = { 'idx', 'descType', 'chance' },
        descType = { 'descType', 'chance' },
        chance = { 'chance' }
    }
    local rowSuffix = ''
    for i, spAttRow in ipairs(spAttTable) do
        local spIdx = spAttRow.idx
        local spAtt = attacks[spIdx]
        -- Determine if it's the first row for any of our groupings
        local resetKeys = {}
        for key, val in pairs(prevRowVal) do
            if spAttRow[key] ~= prevRowVal[key] then
                for j, keyName in ipairs(resetOnChange[key]) do
                    resetKeys[keyName] = true
                end
            end
            prevRowVal[key] = spAttRow[key]
        end
        for key, val in pairs(firstRow) do
            firstRow[key] = (resetKeys[key] ~= nil)
        end

        table.insert(resultPart, '\r\n|-')
        if firstRow.idx then
            rowSuffix = (rowCounts[spIdx]['rows'] > 1 and '|rowspan="' .. rowCounts[spIdx]['rows'] .. '"') or ''
            table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAtt.name)
        end
        table.insert(resultPart, '\r\n|data-sort-value="' .. spAttRow.sourceSort .. '"| ' .. spAttRow.sourceText)
        if includeSource then
            table.insert(resultPart, '\r\n| ' .. spAttRow.source)
        end
        if firstRow.chance then
            rowSuffix = (rowCounts[spIdx][spAttRow.descType][spAttRow.chance] > 1 and 'rowspan="' .. rowCounts[spIdx][spAttRow.descType][spAttRow.chance] .. '" ') or ''
            table.insert(resultPart, '\r\n|' .. rowSuffix .. 'data-sort-value="' .. spAttRow.chance .. '" style="text-align:right;"| ' .. Shared.round(spAttRow.chance, 2, 0) .. '%')
        end
        if firstRow.descType then
            rowSuffix = (rowCounts[spIdx][spAttRow.descType]['rows'] > 1 and '|rowspan="' .. rowCounts[spIdx][spAttRow.descType]['rows'] .. '"') or ''
            local spAttDesc = spAtt['description'][spAttRow.descType]
            --Adding the time between hits and total duration as a note at the end of the special attack description
            local spAttInterval = spAtt.attackInterval ~= nil and spAtt.attackInterval or -1
            if(spAttInterval ~= -1 and spAtt.damage ~= nil and Shared.tableCount(spAtt.damage) > 0) then
            	spAttDesc = spAttDesc..'<br/>('
            	local spAttDuration = spAttInterval * (spAtt.attackCount - 1)
            	spAttDesc = spAttDesc..Shared.round(spAttInterval / 1000, 2, 2)..'s delay between attacks.'
            	if spAtt.attackCount ~= nil and spAtt.attackCount > 1 then
            		spAttDesc = spAttDesc..' '..Shared.round(spAttDuration / 1000, 2, 2)..'s total duration'
            	end
            	spAttDesc = spAttDesc..')'
            end
            table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAttDesc)
        end
    end
    table.insert(resultPart, '\r\n|}')

    return table.concat(resultPart)
end

function p.getSpecialAttackTable(frame)
    local args = frame.args ~= nil and frame.args or frame
    local tableCategories = {'Monster', 'Item', 'Spell', 'Familiar'}
    if args[1] ~= nil and args[1] ~= '' then
        tableCategories = Shared.splitString(args[1], ',')
    end
    local effectName = args['effect']
    local sourceHeaderLabel = (args['sourceHeader'] ~= '' and args['sourceHeader']) or 'Source'
    local includeSource = true
    if args['includeSource'] ~= nil and string.lower(args['includeSource']) == 'false' then
        includeSource = false
    end
    local effectDefn = nil

    if effectName ~= nil and effectName ~= '' then
        effectDefn = Attacks.effectDefinition[effectName]
        if effectDefn == nil then
            local validEffectNames = {}
            for k, v in pairs(Attacks.effectDefinition) do
                table.insert(validEffectNames, k)
            end
            table.sort(validEffectNames, function(a, b) return a < b end)

            return 'ERROR: Invalid effect name "' .. effectName .. '", must be one of: ' .. table.concat(validEffectNames, ', ') .. '[[Category:Pages with script errors]]'
        end
    end

    return p._getSpecialAttackTable(effectDefn, tableCategories, sourceHeaderLabel, includeSource)
end

-- Generates a table showing the damage/DR multipliers for each combat triangle
function p.getCombatTriangleTable()
	local triangleAttributes = {
		{
			["name"] = 'damageModifier',
			["head"] = 'DMG',
			["func"] = function(val)
				local outVal = 100 * (val - 1)
				return { outVal, (outVal < 0 and '' or '+') .. string.format(outVal, '%.0f') .. '%' }
			end
		},
		{
			["name"] = 'reductionModifier',
			["head"] = 'DR',
			["func"] = function(val) return { (val - 1), string.format('%.2fx', val) } end
		}
	}
	local combatStyles = {
		{ 'melee', Icons.Icon({ 'Attack', 'Melee', type = 'skill' }) },
		{ 'ranged', Icons.Icon({ 'Ranged', type = 'skill' }) },
		{ 'magic', Icons.Icon({ 'Magic', type = 'skill' }) }
	}
	local gameMode = {
		{ 'Standard', 'Standard' },
		{ 'Hardcore', Icons.Icon({ 'Hardcore' }) }
	}
	local attrCount = Shared.tableCount(triangleAttributes)
	local styleCount = Shared.tableCount(combatStyles)
	local modeCount = Shared.tableCount(gameMode)
	
	local resultPart = {}
	-- Generate header
	table.insert(resultPart, '{| class="wikitable"\r\n|-')
	table.insert(resultPart, '\r\n!rowspan="2"| Player Style')
	table.insert(resultPart, '\r\n!rowspan="2"| Game Mode')
	for i, style in ipairs(combatStyles) do
		table.insert(resultPart, '\r\n!colspan="' .. attrCount .. '"| VS ' .. style[2])
	end
	local attrHeader = ''
	for i, attr in ipairs(triangleAttributes) do
		attrHeader = attrHeader .. '\r\n! ' .. attr.head
	end
	table.insert(resultPart, '\r\n|-' .. string.rep(attrHeader, styleCount))
	
	-- Generate table body
	for i, attStyle in ipairs(combatStyles) do
		local borderStyle = (i < styleCount and 'style="border-bottom:solid lightgrey"' or '')
		for j, mode in ipairs(gameMode) do
			table.insert(resultPart, '\r\n|-')
			if j == 1 then
				table.insert(resultPart, '\r\n|rowspan="' .. modeCount .. '" ' .. borderStyle .. '| ' .. attStyle[2])
			elseif j == modeCount and borderStyle ~= '' then
				table.insert(resultPart, ' ' .. borderStyle)
			end
			table.insert(resultPart, '\r\n| ' .. mode[2])
			for k, targStyle in ipairs(combatStyles) do
				for m, attr in ipairs(triangleAttributes) do
					local cellStyle = nil
					local attValRaw = Constants.getTriangleAttribute(attr.name, attStyle[1], targStyle[1], mode[1])
					local attrVal = attr.func(attValRaw)
					if attrVal[1] > 0 then
						cellStyle = 'background-color:lightgreen;'
					elseif attrVal[1] < 0 then
						cellStyle = 'background-color:lightpink;'
					end
					table.insert(resultPart, '\r\n|' .. (cellStyle ~= nil and 'style="' .. cellStyle .. '"| ' or ' ') .. attrVal[2])
				end
			end
		end
	end
	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

return p