Difference between revisions of "Module:Attacks/Tables"

From Melvor Idle
m (typing is hard.)
(Use tabs instead of spaces for indentation)
Line 10: Line 10:
  
 
function p._getSpecialAttackTable(effectDefn, categories, sourceHeaderLabel, includeSource)
 
function p._getSpecialAttackTable(effectDefn, categories, sourceHeaderLabel, includeSource)
    local spAttTable = {}
+
local spAttTable = {}
    local attacks = Attacks.getAttacks(function(attack)
+
local attacks = Attacks.getAttacks(function(attack)
                                          if effectDefn == nil then
+
if effectDefn == nil then
                                              return true
+
return true
                                          else
+
else
                                              return Attacks.attackHasEffect(attack, effectDefn)
+
return Attacks.attackHasEffect(attack, effectDefn)
                                          end
+
end
                                      end)
+
end)
    local includeCat = {}
+
local includeCat = {}
    for i, category in ipairs(categories) do
+
for i, category in ipairs(categories) do
        includeCat[category] = true
+
includeCat[category] = true
    end
+
end
  
    -- Compile a list of monsters, items, spells, etc. for each included attack
+
-- Compile a list of monsters, items, spells, etc. for each included attack
    for i, spAtt in ipairs(attacks) do
+
for i, spAtt in ipairs(attacks) do
        -- Monsters
+
-- Monsters
        if includeCat['Monster'] then
+
if includeCat['Monster'] then
            for j, monsterID in ipairs(spAtt.monsters) do
+
for j, monsterID in ipairs(spAtt.monsters) do
                local monster = Monsters.getMonsterByID(monsterID)
+
local monster = Monsters.getMonsterByID(monsterID)
                local overrideChance = (monster.overrideSpecialChances ~= nil and Shared.tableCount(monster.overrideSpecialChances) > 0)
+
local overrideChance = (monster.overrideSpecialChances ~= nil and Shared.tableCount(monster.overrideSpecialChances) > 0)
                local attChance = spAtt.defaultChance
+
local attChance = spAtt.defaultChance
                if overrideChance then
+
if overrideChance then
                    local attIdx = nil
+
local attIdx = nil
                    for k, monsterAttack in ipairs(monster.specialAttacks) do
+
for k, monsterAttack in ipairs(monster.specialAttacks) do
                        local attID = (type(monsterAttack) == 'table' and monsterAttack.id) or monsterAttack
+
local attID = (type(monsterAttack) == 'table' and monsterAttack.id) or monsterAttack
                        if spAtt.id == attID then
+
if spAtt.id == attID then
                            attIdx = k
+
attIdx = k
                            break
+
break
                        end
+
end
                    end
+
end
                    if attIdx ~= nil then
+
if attIdx ~= nil then
                        attChance = monster.overrideSpecialChances[attIdx]
+
attChance = monster.overrideSpecialChances[attIdx]
                    end
+
end
                end
+
end
  
                table.insert(spAttTable, { idx = i, source = 'Monster', sourceSort = monster.name, sourceText = Icons.Icon({ monster.name, type = 'monster' }), chance = attChance, descType = 'monster' })
+
table.insert(spAttTable, { idx = i, source = 'Monster', sourceSort = monster.name, sourceText = Icons.Icon({ monster.name, type = 'monster' }), chance = attChance, descType = 'monster' })
            end
+
end
        end
+
end
  
        -- Items/Weapons
+
-- Items/Weapons
        if includeCat['Item'] then
+
if includeCat['Item'] then
            for j, itemID in ipairs(spAtt.items) do
+
for j, itemID in ipairs(spAtt.items) do
                local item = Items.getItemByID(itemID)
+
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' })
+
table.insert(spAttTable, { idx = i, source = 'Weapon', sourceSort = item.name, sourceText = Icons.Icon({ item.name, type = 'item' }), chance = spAtt.defaultChance, descType = 'player' })
            end
+
end
        end
+
end
  
        -- Spells
+
-- Spells
        if includeCat['Spell'] then
+
if includeCat['Spell'] then
            for j, spellID in ipairs(spAtt.spells) do
+
for j, spellID in ipairs(spAtt.spells) do
                local spell = Magic.getSpellByID(spellID[1], spellID[2])
+
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' })
+
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
+
end
    end
+
end
  
    -- Summoning familiars. Any effects inflicted by combat familiars aren't actually special
+
-- 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
+
-- attacks, therefore the handling here is a bit different and outside of the above attack loop
    if includeCat['Familiar'] then
+
if includeCat['Familiar'] then
        local famIdx = Shared.tableCount(attacks) + 1
+
local famIdx = Shared.tableCount(attacks) + 1
        local familiars = Items.getItems(function(item)
+
local familiars = Items.getItems(function(item)
                                            if item.type == 'Familiar' and Items._getItemStat(item, 'summoningMaxhit') ~= nil and item.modifiers ~= nil then
+
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 } } }
+
local famAttack = { prehitEffects = {}, onhitEffects = { { type = 'Modifier', subtype = 'Familiar', modifiers = item.modifiers } } }
                                                if effectDefn == nil then
+
if effectDefn == nil then
                                                    return Shared.tableCount(Attacks.getAttackEffects(famAttack)) > 0
+
return Shared.tableCount(Attacks.getAttackEffects(famAttack)) > 0
                                                else
+
else
                                                    return Attacks.attackHasEffect(famAttack, effectDefn)
+
return Attacks.attackHasEffect(famAttack, effectDefn)
                                                end
+
end
                                            end
+
end
                                            return false
+
return false
                                        end)
+
end)
        for j, familiar in ipairs(familiars) do
+
for j, familiar in ipairs(familiars) do
            -- For chance, assume the first modifier we come across has the chance, which is pretty lazy
+
-- For chance, assume the first modifier we come across has the chance, which is pretty lazy
            local famChance, famDesc = 0, ''
+
local famChance, famDesc = 0, ''
            for modName, modVal in pairs(familiar.modifiers) do
+
for modName, modVal in pairs(familiar.modifiers) do
                if type(modVal) == 'table' and type(modVal[1]) == 'number' then
+
if type(modVal) == 'table' and type(modVal[1]) == 'number' then
                    famChance = modVal[1]
+
famChance = modVal[1]
                elseif type(modVal) == 'number' then
+
elseif type(modVal) == 'number' then
                    famChance = modVal
+
famChance = modVal
                else
+
else
                    famChance = 0
+
famChance = 0
                end
+
end
                famDesc = Constants._getModifierText(modName, modVal, false)
+
famDesc = Constants._getModifierText(modName, modVal, false)
                break
+
break
            end
+
end
  
            table.insert(spAttTable, { idx = famIdx, source = 'Familiar', sourceSort = familiar.name, sourceText = Icons.Icon({ familiar.name, type = 'item' }), chance = famChance or 0, descType = 'player' })
+
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
+
-- Slap a dummy entry into the attacks table for this familiar
            attacks[famIdx] = { name = familiar.name .. ' (Familiar)', description = { player = famDesc } }
+
attacks[famIdx] = { name = familiar.name .. ' (Familiar)', description = { player = famDesc } }
            famIdx = famIdx + 1
+
famIdx = famIdx + 1
        end
+
end
    end
+
end
  
    -- Nothing to output if there are no row definitions
+
-- Nothing to output if there are no row definitions
    if Shared.tableCount(spAttTable) == 0 then
+
if Shared.tableCount(spAttTable) == 0 then
    return ''
+
return ''
    end
+
end
  
    -- Sort entries into desired order and generate stats to determine row spans:
+
-- 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.)
+
-- By attack index, description type (monster/player), chance, source, then source name (weapon/item/etc.)
    table.sort(spAttTable, function (a, b)
+
table.sort(spAttTable, function (a, b)
                              local sortKeys = { 'idx', 'descType', 'chance', 'source', 'sourceSort' }
+
local sortKeys = { 'idx', 'descType', 'chance', 'source', 'sourceSort' }
                              for i, key in ipairs(sortKeys) do
+
for i, key in ipairs(sortKeys) do
                                  if a[key] ~= b[key] then
+
if a[key] ~= b[key] then
                                      return a[key] < b[key]
+
return a[key] < b[key]
                                  end
+
end
                              end
+
end
                              return false
+
return false
                          end)
+
end)
    -- Determine row counts for grouping/rowspans
+
-- Determine row counts for grouping/rowspans
    local rowCounts = {}
+
local rowCounts = {}
    for i, rowDefn in ipairs(spAttTable) do
+
for i, rowDefn in ipairs(spAttTable) do
        local idx, dt, chance = rowDefn.idx, rowDefn.descType, rowDefn.chance
+
local idx, dt, chance = rowDefn.idx, rowDefn.descType, rowDefn.chance
        if rowCounts[idx] == nil then
+
if rowCounts[idx] == nil then
            rowCounts[idx] = { rows = 0 }
+
rowCounts[idx] = { rows = 0 }
        end
+
end
        if rowCounts[idx][dt] == nil then
+
if rowCounts[idx][dt] == nil then
            rowCounts[idx][dt] = { rows = 0 }
+
rowCounts[idx][dt] = { rows = 0 }
        end
+
end
        if rowCounts[idx][dt][chance] == nil then
+
if rowCounts[idx][dt][chance] == nil then
            rowCounts[idx][dt][chance] = 0
+
rowCounts[idx][dt][chance] = 0
        end
+
end
        rowCounts[idx]['rows'] = rowCounts[idx]['rows'] + 1
+
rowCounts[idx]['rows'] = rowCounts[idx]['rows'] + 1
        rowCounts[idx][dt]['rows'] = rowCounts[idx][dt]['rows'] + 1
+
rowCounts[idx][dt]['rows'] = rowCounts[idx][dt]['rows'] + 1
        rowCounts[idx][dt][chance] = rowCounts[idx][dt][chance] + 1
+
rowCounts[idx][dt][chance] = rowCounts[idx][dt][chance] + 1
    end
+
end
  
    -- Generate output table
+
-- Generate output table
    local resultPart = {}
+
local resultPart = {}
    table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
+
table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
    table.insert(resultPart, '\r\n|- class="headerRow-0"')
+
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')
+
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 firstRow = { idx = true, descType = true, chance = true }
    local prevRowVal = { idx = 0, descType = '', chance = 0 }
+
local prevRowVal = { idx = 0, descType = '', chance = 0 }
    local resetOnChange = {
+
local resetOnChange = {
        idx = { 'idx', 'descType', 'chance' },
+
idx = { 'idx', 'descType', 'chance' },
        descType = { 'descType', 'chance' },
+
descType = { 'descType', 'chance' },
        chance = { 'chance' }
+
chance = { 'chance' }
    }
+
}
    local rowSuffix = ''
+
local rowSuffix = ''
    for i, spAttRow in ipairs(spAttTable) do
+
for i, spAttRow in ipairs(spAttTable) do
        local spIdx = spAttRow.idx
+
local spIdx = spAttRow.idx
        local spAtt = attacks[spIdx]
+
local spAtt = attacks[spIdx]
        -- Determine if it's the first row for any of our groupings
+
-- Determine if it's the first row for any of our groupings
        local resetKeys = {}
+
local resetKeys = {}
        for key, val in pairs(prevRowVal) do
+
for key, val in pairs(prevRowVal) do
            if spAttRow[key] ~= prevRowVal[key] then
+
if spAttRow[key] ~= prevRowVal[key] then
                for j, keyName in ipairs(resetOnChange[key]) do
+
for j, keyName in ipairs(resetOnChange[key]) do
                    resetKeys[keyName] = true
+
resetKeys[keyName] = true
                end
+
end
            end
+
end
            prevRowVal[key] = spAttRow[key]
+
prevRowVal[key] = spAttRow[key]
        end
+
end
        for key, val in pairs(firstRow) do
+
for key, val in pairs(firstRow) do
            firstRow[key] = (resetKeys[key] ~= nil)
+
firstRow[key] = (resetKeys[key] ~= nil)
        end
+
end
  
        table.insert(resultPart, '\r\n|-')
+
table.insert(resultPart, '\r\n|-')
        if firstRow.idx then
+
if firstRow.idx then
            rowSuffix = (rowCounts[spIdx]['rows'] > 1 and '|rowspan="' .. rowCounts[spIdx]['rows'] .. '"') or ''
+
rowSuffix = (rowCounts[spIdx]['rows'] > 1 and '|rowspan="' .. rowCounts[spIdx]['rows'] .. '"') or ''
            table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAtt.name)
+
table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAtt.name)
        end
+
end
        table.insert(resultPart, '\r\n|data-sort-value="' .. spAttRow.sourceSort .. '"| ' .. spAttRow.sourceText)
+
table.insert(resultPart, '\r\n|data-sort-value="' .. spAttRow.sourceSort .. '"| ' .. spAttRow.sourceText)
        if includeSource then
+
if includeSource then
            table.insert(resultPart, '\r\n| ' .. spAttRow.source)
+
table.insert(resultPart, '\r\n| ' .. spAttRow.source)
        end
+
end
        if firstRow.chance then
+
if firstRow.chance then
            rowSuffix = (rowCounts[spIdx][spAttRow.descType][spAttRow.chance] > 1 and 'rowspan="' .. rowCounts[spIdx][spAttRow.descType][spAttRow.chance] .. '" ') or ''
+
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) .. '%')
+
table.insert(resultPart, '\r\n|' .. rowSuffix .. 'data-sort-value="' .. spAttRow.chance .. '" style="text-align:right;"| ' .. Shared.round(spAttRow.chance, 2, 0) .. '%')
        end
+
end
        if firstRow.descType then
+
if firstRow.descType then
            rowSuffix = (rowCounts[spIdx][spAttRow.descType]['rows'] > 1 and '|rowspan="' .. rowCounts[spIdx][spAttRow.descType]['rows'] .. '"') or ''
+
rowSuffix = (rowCounts[spIdx][spAttRow.descType]['rows'] > 1 and '|rowspan="' .. rowCounts[spIdx][spAttRow.descType]['rows'] .. '"') or ''
            local spAttDesc = spAtt['description'][spAttRow.descType]
+
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
+
--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
+
local spAttInterval = spAtt.attackInterval ~= nil and spAtt.attackInterval or -1
            if(spAttInterval ~= -1 and spAtt.damage ~= nil and Shared.tableCount(spAtt.damage) > 0) then
+
if(spAttInterval ~= -1 and spAtt.damage ~= nil and Shared.tableCount(spAtt.damage) > 0) then
            spAttDesc = spAttDesc..'<br/>('
+
spAttDesc = spAttDesc..'<br/>('
            local spAttDuration = spAttInterval * (spAtt.attackCount - 1)
+
local spAttDuration = spAttInterval * (spAtt.attackCount - 1)
            spAttDesc = spAttDesc..Shared.round(spAttInterval / 1000, 2, 2)..'s delay between attacks.'
+
spAttDesc = spAttDesc..Shared.round(spAttInterval / 1000, 2, 2)..'s delay between attacks.'
            if spAtt.attackCount ~= nil and spAtt.attackCount > 2 then
+
if spAtt.attackCount ~= nil and spAtt.attackCount > 2 then
            spAttDesc = spAttDesc..' '..Shared.round(spAttDuration / 1000, 2, 2)..'s total duration'
+
spAttDesc = spAttDesc..' '..Shared.round(spAttDuration / 1000, 2, 2)..'s total duration'
            end
+
end
            spAttDesc = spAttDesc..')'
+
spAttDesc = spAttDesc..')'
            end
+
end
            table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAttDesc)
+
table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAttDesc)
        end
+
end
    end
+
end
    table.insert(resultPart, '\r\n|}')
+
table.insert(resultPart, '\r\n|}')
  
    return table.concat(resultPart)
+
return table.concat(resultPart)
 
end
 
end
  
 
function p.getSpecialAttackTable(frame)
 
function p.getSpecialAttackTable(frame)
    local args = frame.args ~= nil and frame.args or frame
+
local args = frame.args ~= nil and frame.args or frame
    local tableCategories = {'Monster', 'Item', 'Spell', 'Familiar'}
+
local tableCategories = {'Monster', 'Item', 'Spell', 'Familiar'}
    if args[1] ~= nil and args[1] ~= '' then
+
if args[1] ~= nil and args[1] ~= '' then
        tableCategories = Shared.splitString(args[1], ',')
+
tableCategories = Shared.splitString(args[1], ',')
    end
+
end
    local effectName = args['effect']
+
local effectName = args['effect']
    local sourceHeaderLabel = (args['sourceHeader'] ~= '' and args['sourceHeader']) or 'Source'
+
local sourceHeaderLabel = (args['sourceHeader'] ~= '' and args['sourceHeader']) or 'Source'
    local includeSource = true
+
local includeSource = true
    if args['includeSource'] ~= nil and string.lower(args['includeSource']) == 'false' then
+
if args['includeSource'] ~= nil and string.lower(args['includeSource']) == 'false' then
        includeSource = false
+
includeSource = false
    end
+
end
    local effectDefn = nil
+
local effectDefn = nil
  
    if effectName ~= nil and effectName ~= '' then
+
if effectName ~= nil and effectName ~= '' then
        effectDefn = Attacks.effectDefinition[effectName]
+
effectDefn = Attacks.effectDefinition[effectName]
        if effectDefn == nil then
+
if effectDefn == nil then
            local validEffectNames = {}
+
local validEffectNames = {}
            for k, v in pairs(Attacks.effectDefinition) do
+
for k, v in pairs(Attacks.effectDefinition) do
                table.insert(validEffectNames, k)
+
table.insert(validEffectNames, k)
            end
+
end
            table.sort(validEffectNames, function(a, b) return a < b 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]]'
+
return 'ERROR: Invalid effect name "' .. effectName .. '", must be one of: ' .. table.concat(validEffectNames, ', ') .. '[[Category:Pages with script errors]]'
        end
+
end
    end
+
end
  
    return p._getSpecialAttackTable(effectDefn, tableCategories, sourceHeaderLabel, includeSource)
+
return p._getSpecialAttackTable(effectDefn, tableCategories, sourceHeaderLabel, includeSource)
 
end
 
end
  

Revision as of 00:03, 7 January 2022

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 > 2 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