Module:Attacks/Tables: Difference between revisions

From Melvor Idle
(Don't return table when number of attacks is zero)
m (Fixed Spell category and interval between attacks will only show if attack count > 1)
 
(20 intermediate revisions by 4 users not shown)
Line 1: Line 1:
local p = {}
local p = {}


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


function p._getMonsterSpecialAttackTable(effectDefn)
function p._getSpecialAttackTable(effectDefn, categories, sourceHeaderLabel, includeSource)
    local spAttTable = {}
-- TODO Spells and Familiars need fixing for V1.3
    local attacks = Attacks.getAttacks(function(attack)
-- Is going to be incredibly broken following combat effects & modifiers overhaul
                                          if effectDefn == nil then
                                              return true
                                          else
                                              return Attacks.attackHasEffect(attack, effectDefn)
                                          end
                                      end)


    -- Compile table of monsters & chances for each attack
local spAttTable = {}
    for i, spAtt in ipairs(attacks) do
local attacks = Attacks.getAttacks(function(attack)
        if Shared.tableCount(spAtt.monsters) > 0 then
if effectDefn == nil then
            spAttTable[i] = {}
return true
else
return Attacks.attackHasEffect(attack, effectDefn)
end
end)
local includeCat = {}
for i, category in ipairs(categories) do
includeCat[category] = true
end


            for j, monsterID in ipairs(spAtt.monsters) do
    -- Build index so we can tell which to include later
                local monster = Monsters.getMonsterByID(monsterID)
    local includedAttacks = {}
                 local overrideChance = (monster.overrideSpecialChances ~= nil and Shared.tableCount(monster.overrideSpecialChances) > 0)
    for i, attack in ipairs(attacks) do
                 local attChance = spAtt.defaultChance
        includedAttacks[attack.id] = true
                if overrideChance then
    end
                    local attIdx = nil
 
                    for k, monsterAttack in ipairs(monster.specialAttacks) do
-- Compile a list of monsters, items, spells, etc. for each included attack
                         local attID = (type(monsterAttack) == 'table' and monsterAttack.id) or monsterAttack
    -- Monsters
                         if spAtt.id == attID then
    if includeCat['Monster'] then
                             attIdx = k
        for i, monster in ipairs(GameData.rawData.monsters) do
                             break
            if monster.specialAttacks ~= nil and not Shared.tableIsEmpty(monster.specialAttacks) then
                 local overrideChance = (monster.overrideSpecialChances ~= nil and not Shared.tableIsEmpty(monster.overrideSpecialChances))
                 for j, spAttID in ipairs(monster.specialAttacks) do
                    if includedAttacks[spAttID] then
                         local spAtt = GameData.getEntityByID('attacks', spAttID)
                         if spAtt ~= nil then
                             local attChance = (overrideChance and monster.overrideSpecialChances[j]) or spAtt.defaultChance
                             table.insert(spAttTable, { id = spAttID, source = 'Monster', sourceSort = monster.name, sourceText = Icons.Icon({ monster.name, type = 'monster' }), chance = attChance, descType = 'monster' })
                         end
                         end
                    end
                    if attIdx ~= nil then
                        attChance = monster.overrideSpecialChances[attIdx]
                     end
                     end
                 end
                 end
                if spAttTable[i][attChance] == nil then
                    spAttTable[i][attChance] = {}
                end
                table.insert(spAttTable[i][attChance], Icons.Icon({ monster.name, type = 'monster' }))
             end
             end
         end
         end
     end
     end


     -- Generate output table
     -- Items/Weapons
     if Shared.tableCount(spAttTable) == 0 then
     if includeCat['Item'] then
    return ''
        for i, item in ipairs(GameData.rawData.items) do
    end
            if item.specialAttacks ~= nil and not item.golbinRaidExclusive and not Shared.tableIsEmpty(item.specialAttacks) then
   
                local overrideChance = (item.overrideSpecialChances ~= nil and not Shared.tableIsEmpty(item.overrideSpecialChances))
    local resultPart = {}
                for j, spAttID in ipairs(item.specialAttacks) do
    table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
                    if includedAttacks[spAttID] then
    table.insert(resultPart, '\r\n|- class="headerRow-0"')
                        local spAtt = GameData.getEntityByID('attacks', spAttID)
    table.insert(resultPart, '\r\n!Name!!style="min-width:225px"|Monsters!!Chance!!Effect')
                        if spAtt ~= nil then
 
                            local attChance = (overrideChance and item.overrideSpecialChances[j]) or spAtt.defaultChance
    for i, spAttData in Shared.skpairs(spAttTable) do
                            table.insert(spAttTable, { id = spAttID, source = 'Weapon', sourceSort = item.name, sourceText = Icons.Icon({ item.name, type = 'item' }), chance = attChance, descType = 'player' })
        local spAtt = attacks[i]
                        end
        local firstRow = true
                    end
        local rowsSpanned = Shared.tableCount(spAttData)
                 end
        local rowSuffix = ''
        if rowsSpanned > 1 then
            rowSuffix = '|rowspan="' .. rowsSpanned .. '"'
        end
        for chance, iconList in Shared.skpairs(spAttData) do
            table.insert(resultPart, '\r\n|-')
            if firstRow then
                table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAtt.name)
            end
            table.insert(resultPart, '\r\n|data-sort-value="' .. spAtt.name .. '"| ' .. table.concat(iconList, '<br/>'))
            table.insert(resultPart, '\r\n|data-sort-value="' .. chance .. '"| ' .. Shared.round(chance, 2, 0) .. '%')
            if firstRow then
                table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAtt.description.monster)
                 firstRow = false
             end
             end
         end
         end
     end
     end
    table.insert(resultPart, '\r\n|}')


     return table.concat(resultPart)
     -- Spells
end
    if includeCat['Spell'] then
 
        local spellCats = { 'standard', 'ancient', 'archaic', 'abyssal' }
function p._getItemSpecialAttackTable(effectDefn)
        for i, spellCat in ipairs(spellCats) do
  local spAttTable = {}
             for j, spell in ipairs(Magic.getSpellsBySpellBook(spellCat)) do
  local attacks = Attacks.getAttacks(function(attack)
            local spAttID = spell.specialAttack or spell.specialAttackID
                                        if effectDefn == nil then
                 if spAttID ~= nil and includedAttacks[spAttID] then
                                            return true
                    local spAtt = GameData.getEntityByID('attacks', spAttID)
                                        else
                    if spAtt ~= nil then
                                            return Attacks.attackHasEffect(attack, effectDefn)
                        table.insert(spAttTable, {id = spAtt.id, source = 'Spell', sourceSort = spell.name, sourceText = Icons.Icon({ spell.name, type = 'spell' }), chance = spAtt.defaultChance, descType = 'player' })
                                        end
                    end
                                    end)
 
    -- Compile table of items for each attack
    for i, spAtt in ipairs(attacks) do
        if Shared.tableCount(spAtt.items) > 0 then
            spAttTable[i] = { Icons = {} }
 
             for j, itemID in ipairs(spAtt.items) do
                local item = Items.getItemByID(itemID)
                 if spAttTable[i]['sortName'] == nil then
                    spAttTable[i]['sortName'] = item.name
                 end
                 end
                table.insert(spAttTable[i]['Icons'], Icons.Icon({ item.name, type = 'item' }))
             end
             end
         end
         end
     end
     end
--[[
-- Summoning familiars. Any effects inflicted by combat familiars aren't actually special
-- attacks, therefore the handling here is a bit different
if includeCat['Familiar'] then
local famIdx = Shared.tableCount(attacks) + 1
local familiars = Items.getItems(
            function(item)
                if item.type == 'Familiar' and item.modifiers ~= nil and Items._getItemStat(item, 'summoningMaxhit') ~= nil then
                    local famAttack = { prehitEffects = {}, onhitEffects = { { type = 'Modifier', subtype = 'Familiar', modifiers = item.modifiers } } }
                    if effectDefn == nil then
                        return Shared.tableIsEmpty(Attacks.getAttackEffects(famAttack))
                    else
                        return Attacks.attackHasEffect(famAttack, effectDefn)
                    end
                end
                return false
            end)
for i, 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


    -- Generate output table
table.insert(spAttTable, { id = familiar.id, source = 'Familiar', sourceSort = familiar.name, sourceText = Icons.Icon({ familiar.name, type = 'item' }), chance = famChance or 0, descType = 'player' })
    if Shared.tableCount(spAttTable) == 0 then
-- Slap a dummy entry into the attacks table for this familiar
    return ''
attacks[famIdx] = { id = familiar.id, name = familiar.name .. ' (Familiar)', description = famDesc }
    end
famIdx = famIdx + 1
end
end
--]]
-- Nothing to output if there are no row definitions
if Shared.tableIsEmpty(spAttTable) 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 = { 'id', '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 id, dt, chance = rowDefn.id, rowDefn.descType, rowDefn.chance
if rowCounts[id] == nil then
rowCounts[id] = { rows = 0 }
end
if rowCounts[id][dt] == nil then
rowCounts[id][dt] = { rows = 0 }
end
if rowCounts[id][dt][chance] == nil then
rowCounts[id][dt][chance] = 0
end
rowCounts[id]['rows'] = rowCounts[id]['rows'] + 1
rowCounts[id][dt]['rows'] = rowCounts[id][dt]['rows'] + 1
rowCounts[id][dt][chance] = rowCounts[id][dt][chance] + 1
end


    local resultPart = {}
-- Generate output table
    table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
local resultPart = {}
    table.insert(resultPart, '\r\n|- class="headerRow-0"')
table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
    table.insert(resultPart, '\r\n!style="min-width:100px"|Name!!style="min-width:180px"|Weapon(s)!!Chance!!Effect')
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')


    for i, spAttData in Shared.skpairs(spAttTable) do
local firstRow = { id = true, descType = true, chance = true }
        local spAtt = attacks[i]
local prevRowVal = { id = 0, descType = '', chance = 0 }
        table.sort(spAttData.Icons, function(a, b) return a < b end)
local resetOnChange = {
id = { 'id', 'descType', 'chance' },
descType = { 'descType', 'chance' },
chance = { 'chance' }
}
local rowSuffix = ''
for i, spAttRow in ipairs(spAttTable) do
local spIdx = spAttRow.id
local spAtt = GameData.getEntityByID(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|-')
table.insert(resultPart, '\r\n|-')
        table.insert(resultPart, '\r\n| ' .. spAtt.name)
if firstRow.id then
        table.insert(resultPart, '\r\n|data-sort-value="' .. spAttData.sortName .. '"| ' .. table.concat(spAttData.Icons, '<br/>'))
rowSuffix = (rowCounts[spIdx]['rows'] > 1 and '|rowspan="' .. rowCounts[spIdx]['rows'] .. '"') or ''
        table.insert(resultPart, '\r\n|data-sort-value="' .. spAtt.defaultChance .. '"| ' .. spAtt.defaultChance .. '%')
table.insert(resultPart, '\r\n' .. rowSuffix .. '| ' .. spAtt.name)
        table.insert(resultPart, '\r\n| ' .. spAtt.description.player)
end
    end
table.insert(resultPart, '\r\n|data-sort-value="' .. spAttRow.sourceSort .. '"| ' .. spAttRow.sourceText)
    table.insert(resultPart, '\r\n|}')
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;"| ' .. Num.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
--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 and spAtt.attackCount > 1) then
spAttDesc = spAttDesc..'<br/>('
local spAttDuration = spAttInterval * (spAtt.attackCount - 1)
spAttDesc = spAttDesc..Num.round(spAttInterval / 1000, 2, 2)..'s delay between attacks.'
if spAtt.attackCount ~= nil and spAtt.attackCount > 2 then
spAttDesc = spAttDesc..' '..Num.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)
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 tableType = args[1]
local tableCategories = {'Monster', 'Item', 'Spell', 'Familiar'}
    local effectName = args[2]
if args[1] ~= nil and args[1] ~= '' then
    local effectDefn = nil
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
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 Shared.printError('Invalid effect name "' .. effectName .. '", must be one of: ' .. table.concat(validEffectNames, ', '))
        end
end
    end
end


    if tableType == 'Monster' then
return p._getSpecialAttackTable(effectDefn, tableCategories, sourceHeaderLabel, includeSource)
        return p._getMonsterSpecialAttackTable(effectDefn)
    elseif tableType == 'Item' then
        return p._getItemSpecialAttackTable(effectDefn)
    else
        return 'ERROR: Invalid table type, must be one of: Item, Monster[[Category:Pages with script errors]]'
    end
end
end


return p
return p

Latest revision as of 01:45, 28 August 2024

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 GameData = require('Module:GameData')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local Attacks = require('Module:Attacks')
local Num = require('Module:Number')
local Magic = require('Module:Magic')

function p._getSpecialAttackTable(effectDefn, categories, sourceHeaderLabel, includeSource)
	-- TODO Spells and Familiars need fixing for V1.3
	-- Is going to be incredibly broken following combat effects & modifiers overhaul

	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

    -- Build index so we can tell which to include later
    local includedAttacks = {}
    for i, attack in ipairs(attacks) do
        includedAttacks[attack.id] = true
    end

	-- Compile a list of monsters, items, spells, etc. for each included attack
    -- Monsters
    if includeCat['Monster'] then
        for i, monster in ipairs(GameData.rawData.monsters) do
            if monster.specialAttacks ~= nil and not Shared.tableIsEmpty(monster.specialAttacks) then
                local overrideChance = (monster.overrideSpecialChances ~= nil and not Shared.tableIsEmpty(monster.overrideSpecialChances))
                for j, spAttID in ipairs(monster.specialAttacks) do
                    if includedAttacks[spAttID] then
                        local spAtt = GameData.getEntityByID('attacks', spAttID)
                        if spAtt ~= nil then
                            local attChance = (overrideChance and monster.overrideSpecialChances[j]) or spAtt.defaultChance
                            table.insert(spAttTable, { id = spAttID, source = 'Monster', sourceSort = monster.name, sourceText = Icons.Icon({ monster.name, type = 'monster' }), chance = attChance, descType = 'monster' })
                        end
                    end
                end
            end
        end
    end

    -- Items/Weapons
    if includeCat['Item'] then
        for i, item in ipairs(GameData.rawData.items) do
            if item.specialAttacks ~= nil and not item.golbinRaidExclusive and not Shared.tableIsEmpty(item.specialAttacks) then
                local overrideChance = (item.overrideSpecialChances ~= nil and not Shared.tableIsEmpty(item.overrideSpecialChances))
                for j, spAttID in ipairs(item.specialAttacks) do
                    if includedAttacks[spAttID] then
                        local spAtt = GameData.getEntityByID('attacks', spAttID)
                        if spAtt ~= nil then
                            local attChance = (overrideChance and item.overrideSpecialChances[j]) or spAtt.defaultChance
                            table.insert(spAttTable, { id = spAttID, source = 'Weapon', sourceSort = item.name, sourceText = Icons.Icon({ item.name, type = 'item' }), chance = attChance, descType = 'player' })
                        end
                    end
                end
            end
        end
    end

    -- Spells
    if includeCat['Spell'] then
        local spellCats = { 'standard', 'ancient', 'archaic', 'abyssal' }
        for i, spellCat in ipairs(spellCats) do
            for j, spell in ipairs(Magic.getSpellsBySpellBook(spellCat)) do
            	local spAttID = spell.specialAttack or spell.specialAttackID
                if spAttID ~= nil and includedAttacks[spAttID] then
                    local spAtt = GameData.getEntityByID('attacks', spAttID)
                    if spAtt ~= nil then
                        table.insert(spAttTable, {id = spAtt.id, source = 'Spell', sourceSort = spell.name, sourceText = Icons.Icon({ spell.name, type = 'spell' }), chance = spAtt.defaultChance, descType = 'player' })
                    end
                end
            end
        end
    end
	--[[
	-- Summoning familiars. Any effects inflicted by combat familiars aren't actually special
	-- attacks, therefore the handling here is a bit different
	if includeCat['Familiar'] then
		local famIdx = Shared.tableCount(attacks) + 1
		local familiars = Items.getItems(
            function(item)
                if item.type == 'Familiar' and item.modifiers ~= nil and Items._getItemStat(item, 'summoningMaxhit') ~= nil then
                    local famAttack = { prehitEffects = {}, onhitEffects = { { type = 'Modifier', subtype = 'Familiar', modifiers = item.modifiers } } }
                    if effectDefn == nil then
                        return Shared.tableIsEmpty(Attacks.getAttackEffects(famAttack))
                    else
                        return Attacks.attackHasEffect(famAttack, effectDefn)
                    end
                end
                return false
            end)
		for i, 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, { id = familiar.id, 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] = { id = familiar.id, name = familiar.name .. ' (Familiar)', description = famDesc }
			famIdx = famIdx + 1
		end
	end
	--]]
	-- Nothing to output if there are no row definitions
	if Shared.tableIsEmpty(spAttTable) 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 = { 'id', '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 id, dt, chance = rowDefn.id, rowDefn.descType, rowDefn.chance
		if rowCounts[id] == nil then
			rowCounts[id] = { rows = 0 }
		end
		if rowCounts[id][dt] == nil then
			rowCounts[id][dt] = { rows = 0 }
		end
		if rowCounts[id][dt][chance] == nil then
			rowCounts[id][dt][chance] = 0
		end
		rowCounts[id]['rows'] = rowCounts[id]['rows'] + 1
		rowCounts[id][dt]['rows'] = rowCounts[id][dt]['rows'] + 1
		rowCounts[id][dt][chance] = rowCounts[id][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 = { id = true, descType = true, chance = true }
	local prevRowVal = { id = 0, descType = '', chance = 0 }
	local resetOnChange = {
		id = { 'id', 'descType', 'chance' },
		descType = { 'descType', 'chance' },
		chance = { 'chance' }
	}
	local rowSuffix = ''
	for i, spAttRow in ipairs(spAttTable) do
		local spIdx = spAttRow.id
		local spAtt = GameData.getEntityByID(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.id 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;"| ' .. Num.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
			--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 and spAtt.attackCount > 1) then
				spAttDesc = spAttDesc..'<br/>('
				local spAttDuration = spAttInterval * (spAtt.attackCount - 1)
				spAttDesc = spAttDesc..Num.round(spAttInterval / 1000, 2, 2)..'s delay between attacks.'
				if spAtt.attackCount ~= nil and spAtt.attackCount > 2 then
					spAttDesc = spAttDesc..' '..Num.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 Shared.printError('Invalid effect name "' .. effectName .. '", must be one of: ' .. table.concat(validEffectNames, ', '))
		end
	end

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

return p