Module:Magic: Difference between revisions

From Melvor Idle
(Add damage column for abyssal spell tables, add initial combat effects handling)
m (Use css class for sub text)
 
(One intermediate revision by the same user not shown)
Line 188: Line 188:
local rune = Items.getItemByID(req.id)
local rune = Items.getItemByID(req.id)
if rune ~= nil then
if rune ~= nil then
local sub = mw.html.create('sub')
local wrapper = mw.html.create('span')
:wikitext(req.quantity)
wrapper:css('white-space', 'nowrap')
:css('vertical-align', 'super')
:tag('sub'):wikitext(req.quantity):addClass('item-qty'):done()
:css('font-size', 'smaller')
:wikitext(Icons.Icon({rune.name, type='item', notext=true}))
:css('margin-right', '1px')
html:node(wrapper)
:css('margin-left', '3px')
:done()
html:node(sub)
html:wikitext(Icons.Icon({rune.name, type='item', notext=true}))
end
end
end
end

Latest revision as of 20:35, 12 December 2024

Data pulled from Module:GameData/data


local p = {}

local Shared = require('Module:Shared')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Common = require('Module:Common')
local Modifiers = require('Module:Modifiers')
local Attacks = require('Module:Attacks')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local Num = require('Module:Number')

p.spellBooks = {
	{ id = 'standard', dataID = 'attackSpells', name = 'Standard Magic', imgType = 'spell', bookID = 'melvorD:Standard' },
	{ id = 'ancient', dataID = 'attackSpells', name = 'Ancient Magick', imgType = 'spell', bookID = 'melvorF:Ancient' },
	{ id = 'archaic', dataID = 'attackSpells', name = 'Archaic Magick', imgType = 'spell', bookID = 'melvorTotH:Archaic' },
	{ id = 'abyssal' , dataID = 'attackSpells', name = 'Abyssal', imgType = 'spell', bookID = 'melvorItA:Abyssal' },
	{ id = 'curse', dataID = 'curseSpells', name = 'Curse', imgType = 'curse' },
	{ id = 'aurora', dataID = 'auroraSpells', name = 'Aurora', imgType = 'aurora' },
	{ id = 'altMagic', dataID = 'altSpells', name = 'Alt. Magic', imgType = 'spell', dataRoot = GameData.getSkillData('melvorD:Magic') }
}

function p.getSpellBookID(sectionName)
	if sectionName == 'Spell' or sectionName == 'Standard' then
		return 'standard'
	elseif sectionName == 'Ancient' then
		return 'ancient'
	elseif sectionName == 'Archaic' then
		return 'archaic'
	elseif sectionName == 'Abyssal' then
		return 'abyssal'
	elseif sectionName == 'Curse' then
		return 'curse'
	elseif sectionName == 'Aurora' then
		return 'aurora'
	elseif Shared.contains({'Alt Magic', 'Alt. Magic', 'Alternative Magic'}, sectionName) then
		return 'altMagic'
	else
		return sectionName
	end
end

-- Retrieves all spells within the given spellbook
function p.getSpellsBySpellBook(spellBookID)
	if type(spellBookID) == 'string' then
		local spellBook = GameData.getEntityByID(p.spellBooks, spellBookID)
		if spellBook ~= nil then
			local dataRoot = spellBook.dataRoot or GameData.rawData
			local spellData = dataRoot[spellBook.dataID]
			if spellBook.bookID == nil then
				return spellData
			else
				return GameData.getEntities(spellData, function(spell) return spell.spellbook == spellBook.bookID end)
			end
		end
	end
end

local spellToSpellbookIdx = {}
for bookIdx, spellBook in ipairs(p.spellBooks) do
	local spells = p.getSpellsBySpellBook(spellBook.id)
	for _, spell in ipairs(spells) do
		spellToSpellbookIdx[spell.id] = bookIdx
	end
end

function p.getSpellBookFromSpell(spell)
	local bookIdx = spellToSpellbookIdx[spell.id]
	if bookIdx ~= nil then
		return p.spellBooks[bookIdx]
	end
end

function p.getSpell(name, spellType)
	return p.getSpellByProperty(Shared.fixPagename(name), 'name', spellType)
end

function p.getSpellByID(spellID, spellType)
	return p.getSpellByProperty(spellID, 'id', spellType)
end

function p.getSpellByProperty(spellProperty, propertyName, spellType)
	if spellType == nil then
		-- Look for spell in all spellbooks
		for _, spellBook in ipairs(p.spellBooks) do
			local spells = p.getSpellsBySpellBook(spellBook.id)
			if spells ~= nil and not Shared.tableIsEmpty(spells) then 
				local spell = GameData.getEntityByProperty(spells, propertyName, spellProperty)
				if spell ~= nil then
					return spell
				end
			end
		end
	else
		local spellBookID = p.getSpellBookID(spellType)
		if spellBookID ~= nil then
			local spells = p.getSpellsBySpellBook(spellBookID)
			if spells ~= nil and not Shared.tableIsEmpty(spells) then 
				return GameData.getEntityByProperty(spells, propertyName, spellProperty)
			end
		end
	end
end

--Returns the expansion icon for the spell if it has one
function p.getExpansionIcon(frame)
	local spellName = frame.args ~= nil and frame.args[1] or frame
	local spell = p.getSpell(spellName)
	if spell == nil then
		return Shared.printError('No spell named "' .. spellName .. '" exists in the data module')
	end

	return Icons.getExpansionIcon(spell.id)
end

function p._getSpellIconType(spell)
	local spellBook = p.getSpellBookFromSpell(spell)
	if spellBook == nil then
		-- Pick a suitable default
		return 'spell'
	else
		return spellBook.imgType
	end
end

function p.getSpellIconType(frame)
	local spellName = frame.args ~= nil and frame.args[1] or frame
	local spell = p.getSpell(spellName)
	if spell == nil then
		return 'spell'
	else
		return p._getSpellIconType(spell)
	end
end

function p._getSpellIcon(spell, size)
	if size == nil then size = 50 end
	local imgType = p._getSpellIconType(spell)
	return Icons.Icon({spell.name, type=imgType, notext=true, size=size})
end

function p._getSpellRequirements(spell)
	-- All spells have a Magic level requirement
	local extraReqs = {}
	if spell.abyssalLevel ~= nil and spell.abyssalLevel > 0 then
		table.insert(extraReqs, {
			['type'] = 'AbyssalLevel',
			['skillID'] = 'melvorD:Magic',
			['level'] = spell.abyssalLevel
		})
	else
		table.insert(extraReqs, {
			['type'] = 'SkillLevel',
			['skillID'] = 'melvorD:Magic',
			['level'] = spell.level
		})
	end
	if spell.requiredItemID ~= nil then
		table.insert(extraReqs, {
			['type'] = 'SlayerItem',
			['itemID'] = spell.requiredItemID
		})
	end

	local resultPart = {}
	for i, reqs in ipairs({ extraReqs, spell.requirements }) do
		local reqStr = Common.getRequirementString(reqs)
		if reqStr ~= nil then
			table.insert(resultPart, reqStr)
		end
	end
	
	-- Note the Smithing level requirement for Superheat spells
	if spell.produces ~= nil and spell.produces == 'Bar' then
		table.insert(resultPart, "Bar's " .. Icons._SkillRealmIcon('Smithing', 'melvorD:Melvor') .. ' Level')
	end

	if Shared.tableIsEmpty(resultPart) then
		return 'None'
	else
		return table.concat(resultPart, '<br/>')
	end
end

local function formatRuneList(runes)
	local html = mw.html.create()
	for i, req in ipairs(runes) do
		local rune = Items.getItemByID(req.id)
		if rune ~= nil then
			local wrapper = mw.html.create('span')
			wrapper:css('white-space', 'nowrap')
				:tag('sub'):wikitext(req.quantity):addClass('item-qty'):done()
				:wikitext(Icons.Icon({rune.name, type='item', notext=true}))
			html:node(wrapper)
		end
	end
	return tostring(html)
end


function p._getSpellItems(spell)
	if type(spell.fixedItemCosts) == 'table' then
		local resultPart = {}
		for i, req in ipairs(spell.fixedItemCosts) do
			local item = Items.getItemByID(req.id)
			if item ~= nil then
				table.insert(resultPart, Icons.Icon({item.name, type='item', qty = req.quantity}))
			end
		end
		return table.concat(resultPart, '<br/>')
	else
		return ''
	end
end

function p.getSpellItems(frame)
	local spellName = frame.args ~= nil and frame.args[1] or frame
	local spell = p.getSpell(spellName)
	if spell == nil then
		return Shared.printError('No spell named "' .. spellName .. '" exists in the data module')
	end
	return p._getSpellItems(spell)
end

function p._getSpellRunes(spell)
	if type(spell.runesRequired) == 'table' then
		local resultPart  = {}
		table.insert(resultPart, formatRuneList(spell.runesRequired))
		if spell.runesRequiredAlt ~= nil and not Shared.tablesEqual(spell.runesRequired, spell.runesRequiredAlt) then
			table.insert(resultPart, "<br/>'''OR'''<br/>" .. formatRuneList(spell.runesRequiredAlt))
		end
		return table.concat(resultPart)
	else
		return ''
	end
end

function p.getSpellRunes(frame)
	local spellName = frame.args ~= nil and frame.args[1] or frame
	local spell = p.getSpell(spellName)
	if spell == nil then
		return Shared.printError('No spell named "' .. spellName .. '" exists in the data module')
	end
	return p._getSpellRunes(spell)
end

-- Generates description template data. See: altMagic.js, description()
function p._getSpellTemplateData(spell)
	local templateData = nil
	local spellBook = p.getSpellBookFromSpell(spell)
	if spellBook.id == 'altMagic' then
		if spell.produces ~= nil then
			-- Item produced varies depending on items consumed
			if spell.produces == 'Bar' then
				templateData = {
					["barAmount"] = spell.productionRatio,
					["oreAmount"] = spell.specialCost.quantity
				}
			elseif spell.produces == 'GP' then
				templateData = {
					["percent"] = spell.productionRatio * 100
				}
			else
				local itemProduced = Items.getItemByID(spell.produces)
				local spellNS, spellLocalID = GameData.getLocalID(spell.id)
				if itemProduced ~= nil and itemProduced.prayerPoints ~= nil and type(spell.fixedItemCosts) == 'table' and Shared.tableCount(spell.fixedItemCosts) == 1 and spellNS ~= 'melvorAoD' then
					-- Item produced is a bone and spell is not from AoD (logic from altMagic.js)
					local costItem = Items.getItemByID(spell.fixedItemCosts[1].id)
					if costItem ~= nil then
						templateData = {
							["itemName"] = costItem.name,
							["qty1"] = spell.fixedItemCosts[1].quantity,
							["qty2"] = itemProduced.prayerPoints
						}
					end
				end
			end
		end
		if templateData == nil then
			templateData = {
				["amount"] = spell.productionRatio,
				["percent"] = spell.productionRatio * 100,
				["specialCostQty"] = spell.specialCost.quantity
			}
			if type(spell.fixedItemCosts) == 'table' then
				for i, fixedCost in ipairs(spell.fixedItemCosts) do
					local item = Items.getItemByID(fixedCost.id)
					if item ~= nil then
						templateData['fixedItemName' .. (i - 1)] = item.name
						templateData['fixedItemQty' .. (i - 1)] = fixedCost.quantity
					end
				end
			end
		end
	end
	return (templateData or {})
end

function p._getSpellDescription(spell, inline)
	if inline == nil then inline = false end
	local connector = inline and '<br/>' or ' and '
	local spellBook = p.getSpellBookFromSpell(spell)
	if spell.description ~= nil then
		return Shared.applyTemplateData(spell.description, p._getSpellTemplateData(spell))
	elseif spell.modifiers ~= nil then
		return Modifiers.getModifiersText(spell.modifiers, false, inline)
	elseif spell.effectID ~= nil then
		local effect = GameData.getEntityByID('combatEffects', spell.effectID)
		if effect ~= nil and effect.statGroups ~= nil then
			for _, statGroup in ipairs(effect.statGroups) do
				if statGroup.modifiers ~= nil then
					return 'Enemies are inflicted with:<br>' .. Modifiers.getModifiersText(statGroup.modifiers or {}, false, inline)
				end
			end
		end
		return ''
	elseif spell.combatEffects ~= nil then
		for _, combatEffect in ipairs(spell.combatEffects) do
			-- Doesn't handle initialParams, which is used by the four abyssal spells
			local effect = GameData.getEntityByID('combatEffects', combatEffect.effectID)
			if effect ~= nil and effect.statGroups ~= nil then
				for _, statGroup in ipairs(effect.statGroups) do
					if statGroup.modifiers ~= nil then
						return 'Enemies are inflicted with:<br>' .. Modifiers.getModifiersText(statGroup.modifiers or {}, false, inline)
					end
				end
			end
		end
		return ''
	elseif spell.specialAttackID ~= nil or spell.specialAttack ~= nil then
		local spAtt = Attacks.getAttackByID(spell.specialAttackID or spell.specialAttack)
		if spAtt ~= nil then
			return spAtt.description
		end
	elseif spellBook.id == 'standard' then
		return 'Combat spell with a max hit of ' .. Num.formatnum(spell.maxHit * 10)
	else
		return ''
	end
end

function p._getSpellStat(spell, stat)
	if stat == 'bigIcon' then
		return p._getSpellIcon(spell, 250)
	elseif stat == 'description' then
		return p._getSpellDescription(spell)
	elseif stat == 'icon' then
		return p._getSpellIcon(spell)
	elseif stat == 'requirements' then
		return p._getSpellRequirements(spell)
	elseif stat == 'runes' then
		return p._getSpellRunes(spell)
	elseif stat == 'type' then
		local spellBook = p.getSpellBookFromSpell(spell)
		return spellBook.name
	elseif stat == 'spellDamage' then
		if spell.maxHit ~= nil then 
			return spell.maxHit * 10
		else
			return 0
		end
	end
	return spell[stat]
end

function p.getSpellStat(frame)
	local spellName = frame.args ~= nil and frame.args[1] or frame[1]
	local statName = frame.args ~= nil and frame.args[2] or frame[2]
	local spell = p.getSpell(spellName)
	if spell == nil then
		return Shared.printError('No spell named "' .. spellName .. '" exists in the data module')
	end
	return p._getSpellStat(spell, statName)
end

function p.getOtherSpellBoxText(frame)
	local spellName = frame.args ~= nil and frame.args[1] or frame
	local spell = p.getSpell(spellName)
	if spell == nil then
		return Shared.printError('No spell named "' .. spellName .. '" exists in the data module')
	end
	local spellBook = p.getSpellBookFromSpell(spell)

	local result = ''

	--11/01/22: Added Spell Damage for standard & archaic spells
	if Shared.contains({'standard', 'archaic', 'abyssal'}, spellBook.id) then
		result = result.."\r\n|-\r\n|'''Spell Damage:''' "..p._getSpellStat(spell, 'spellDamage')
	end
	--8/20/21: Changed to using the new getSpellDescription function
	-- TODO: Spell descriptions need fixing, now uses combat effects rather than modifiers
	local spellDesc = p._getSpellStat(spell, 'description')
	if spellDesc ~= '' then
		result = result.."\r\n|-\r\n|'''Description:'''<br/>"..spellDesc
	end

	return result
end

function p._getSpellCategories(spell)
	local spellBook = p.getSpellBookFromSpell(spell)
	local result = '[[Category:Spells]]'
	result = result..'[[Category:' .. spellBook.name .. ']]'
	return result
end

function p.getSpellCategories(frame)
	local spellName = frame.args ~= nil and frame.args[1] or frame
	local spell = p.getSpell(spellName)
	if spell == nil then
		return Shared.printError('No spell named "' .. spellName .. '" exists in the data module')
	end
	return p._getSpellCategories(spell)
end

function p._getAltSpellCostText(spell)
	if spell.specialCost ~= nil then
		local costType = spell.specialCost.type
		if costType == nil or costType == 'None' then
			if type(spell.fixedItemCosts) == 'table' then
				local costText = {}
				for i, itemCost in ipairs(spell.fixedItemCosts) do
					local item = Items.getItemByID(itemCost.id)
					if item ~= nil then
						table.insert(costText, Icons.Icon({item.name, type='item', qty=itemCost.quantity}))
					end
				end
				if not Shared.tableIsEmpty(costText) then
					return table.concat(costText, ', ')
				end
			else
				return nil
			end
		else
			local qty = Num.formatnum(spell.specialCost.quantity)
			local typeString = {
				['AnyItem'] = qty .. ' of any item',
				['BarIngredientsWithCoal'] = qty .. ' x required ores for the chosen bar',
				['BarIngredientsWithoutCoal'] = qty .. ' x required ores (except ' .. Icons.Icon({'Coal Ore', type='item'}) .. ') for the chosen bar',
				['JunkItem'] = qty .. ' of any [[Fishing#Junk|Junk]] item',
				['SuperiorGem'] = qty .. ' of any superior gem',
				['AnyNormalFood'] = qty .. ' x non-perfect food'
			}
			return typeString[costType]
		end
	end
end

function p.getSpellsProducingItem(itemID)
	-- Only need to check Alt. Magic spells
	local spellList = {}

	-- Classify whether the item fits into various categories
	local isBar, isShard, isGem, isSuperiorGem, isPerfectFood = false, false, false, false, false
	local item = Items.getItemByID(itemID)
	if item ~= nil then
		isBar = not Shared.tableIsEmpty(GameData.getEntities(SkillData.Smithing.recipes,
				function(recipe)
					return recipe.categoryID == 'melvorD:Bars' and recipe.productID == item.id
				end))
		isShard = GameData.getEntityByProperty(SkillData.Magic.randomShards, 'itemID', item.id) ~= nil
		isGem = GameData.getEntityByProperty('randomGems', 'itemID', itemID) ~= nil
		--Runestone can't be created by Alt Magic spells that make random superior gems.
		isSuperiorGem = item.type == 'Superior Gem' and item.id ~= SkillData.Mining.runestoneItemID
		if item.healsFor ~= nil then
			-- Item is food, but is it a product of perfect cooking?
			local cookData = GameData.getSkillData('melvorD:Cooking')
			if cookData ~= nil and cookData.recipes ~= nil then
				isPerfectFood = GameData.getEntityByProperty(cookData.recipes, 'perfectCookID', itemID) ~= nil
			end
		end
	end

	for i, spell in ipairs(p.getSpellsBySpellBook('altMagic')) do
		local includeSpell = false
		if spell.produces ~= nil then
			if spell.produces == itemID then
				includeSpell = true
			else
				includeSpell = ((isBar and spell.produces == 'Bar') or
					(isShard and spell.produces == 'RandomShards') or
					(isGem and spell.produces == 'RandomGem') or
					(isSuperiorGem and spell.produces == 'RandomSuperiorGem') or
					(isPerfectFood and spell.produces == 'PerfectFood'))
			end
			if includeSpell then
				table.insert(spellList, spell)
			end
		end
	end

	table.sort(spellList, function(a, b) return (a.abyssalLevel or a.level) < (b.abyssalLevel or b.level) end)
	return spellList
end

-- If includeConsumes = true, then checks for Alt. Magic spell resource consumptions as well as
-- the rune cost of spells
function p.getSpellsUsingItem(itemID, includeConsumes)
	if type(includeConsumes) ~= 'boolean' then
		includeConsumes = false
	end
	local runeKeys = { 'runesRequired', 'runesRequiredAlt' }
	local spellList = {}
	
	-- Initialize some vars & only populate if we're including resource consumptions
	local isJunkItem, isSuperiorGem, isNormalFood, isCoal, isBarIngredient = false, false, false, false, false
	if includeConsumes then
		local thisItem = Items.getItemByID(itemID)
		local junkItemIDs = GameData.getSkillData('melvorD:Fishing').junkItemIDs
		isJunkItem = Shared.contains(junkItemIDs, itemID)
		isSuperiorGem = thisItem.type == 'Superior Gem'
		if thisItem.healsFor ~= nil then
			-- Item is food, but is it from cooking & is it normal or perfect?
			local cookData = GameData.getSkillData('melvorD:Cooking')
			if cookData ~= nil and cookData.recipes ~= nil then
				isNormalFood = GameData.getEntityByProperty(cookData.recipes, 'productID', itemID) ~= nil
			end
		end
		isCoal = itemID == 'melvorD:Coal_Ore'
		if not isCoal then
			-- Don't need to check if the item is another bar ingredient if we already know it is coal
			local smithingRecipes = GameData.getSkillData('melvorD:Smithing').recipes
			for i, recipe in ipairs(smithingRecipes) do
				if recipe.categoryID == 'melvorD:Bars' then
					for k, itemCost in ipairs(recipe.itemCosts) do
						if itemCost.id == itemID then
							isBarIngredient = true
							break
						end
					end
					if isBarIngredient then
						break
					end
				end
			end
		end
	end

	-- Find applicable spells
	for i, spellBook in ipairs(p.spellBooks) do
		local spells = p.getSpellsBySpellBook(spellBook.id)
		for j, spell in ipairs(spells) do
			local foundSpell = false
			-- Check runes first
			for k, runeKey in ipairs(runeKeys) do
				if spell[runeKey] ~= nil then
					for m, req in ipairs(spell[runeKey]) do
						if req.id == itemID then
							foundSpell = true
							break
						end
					end
				end
				if foundSpell then
					break
				end
			end
			if includeConsumes and not foundSpell then
				-- Check items consumed by the spell
				-- Fixed costs first, as that is a well-defined list of item IDs
				if spell.fixedItemCosts ~= nil then
					for k, itemCost in ipairs(spell.fixedItemCosts) do
						if itemCost.id == itemID then
							foundSpell = true
							break
						end
					end
				end
				if not foundSpell and spell.specialCost ~= nil then
					local costType = spell.specialCost.type
					foundSpell = (isJunkItem and costType == 'JunkItem') or
						(isSuperiorGem and costType == 'AnySuperiorGem') or
						(isNormalFood and costType == 'AnyNormalFood') or
						((isCoal or isBarIngredient) and costType == 'BarIngredientsWithCoal') or
						(isBarIngredient and costType == 'BarIngredientsWithoutCoal')
				end
			end
			
			if foundSpell then
				table.insert(spellList, spell)
			end
		end
	end

	table.sort(spellList, function(a, b)
		local bookA, bookB = p.getSpellBookFromSpell(a), p.getSpellBookFromSpell(b)
		if bookA.id ~= bookB.id then
			return bookA.id < bookB.id
		else
			return (a.abyssalLevel or a.level) < (b.abyssalLevel or b.level)
		end
	end)
	return spellList
end

-- The below function is included for backwards compatibility
function p.getSpellsForRune(runeID)
	return p.getSpellsUsingItem(runeID, false)
end

function p.getSpellTypeLink(spellBookID)
	if spellBookID == 'standard' then
		return Icons.Icon({'Standard Magic', 'Standard', img='Standard', type='spellType'})
	elseif spellBookID == 'ancient' then
		return Icons.Icon({'Ancient Magicks', 'Ancient', img='Ancient', type='spellType'})
	elseif spellBookID == 'archaic' then
		return Icons.Icon({'Archaic Magicks', 'Archaic', img='Archaic', type='spellType'})
	elseif spellBookID == 'abyssal' then
		return Icons.Icon({'Abyssal Magicks', 'Abyssal', img='Abyssal', type='spellType'})
	elseif spellBookID == 'curse' then
		return Icons.Icon({'Curses', 'Curse', img='Curse', type='spellType'})
	elseif spellBookID == 'aurora' then
		return Icons.Icon({'Auroras', 'Aurora', img='Aurora', type='spellType'})
	elseif spellBookID == 'altMagic' then
		return Icons.Icon({'Alt. Magic', type='skill'})
	end
	return ''
end

function p._getSpellHeader(includeTypeColumn, includeItems, includeDamage, includeExperience)

end

function p._getSpellRow(spell, includeTypeColumn, includeItems, includeDamage, includeExperience)

end

function p._getSpellTable(spellList, includeTypeColumn)
	if type(spellList) == 'table' and not Shared.tableIsEmpty(spellList) then
		local includeSpellbook, includeItems, includeDamage, includeExperience = false, false, false, false
		if type(includeTypeColumn) == 'boolean' then
			includeSpellbook = includeTypeColumn
		end
		-- Check to see what columns are required
		for i, spell in ipairs(spellList) do
			local spellBook = p.getSpellBookFromSpell(spell)
			if not includeItems and p._getSpellItems(spell) ~= '' then
				includeItems = true
			end
			if not includeExperience and spellBook.id == 'altMagic' then
				includeExperience = true
			end
			if not includeDamage and Shared.contains({'standard', 'archaic', 'abyssal'}, spellBook.id) then
				includeDamage = true
			end
		end

		local spellListSorted = Shared.shallowClone(spellList)
		table.sort(spellListSorted, function(a, b) return (a.abyssalLevel or a.level) < (b.abyssalLevel or b.level) end)
		
		---- Header stuff ----
		local html = mw.html.create('table')
			:addClass('wikitable sortable stickyHeader')
		
		local header = html:tag('tr')
		header:tag('th'):wikitext('Spell')
						:attr('colspan', 2)

		if includeTypeColumn then
			header:tag('th'):wikitext('Spellbook')
		end
		header:tag('th'):wikitext('Requirements')
		header:tag('th'):wikitext('[[DLC]]')

		if includeDamage then
			header:tag('th'):wikitext('Spell Dmg')
		end
		header:tag('th'):wikitext('Description')
		--table.insert(resultPart, 'style="width:275px"| Description')
		
		if includeExperience then
			header:tag('th'):wikitext('XP')
		end
		
		header:tag('th'):wikitext('Runes')
						:css('min-width', '90px')

		if includeItems then
			header:tag('th'):wikitext('Item Cost')
		end

		---- row stuff ----
		for i, spell in ipairs(spellListSorted) do
			local spellBook = p.getSpellBookFromSpell(spell)
			local row = html:tag('tr')
			row:tag('td'):wikitext(Icons.Icon({spell.name, type=spellBook.imgType, notext=true}))
						 :css('text-align', 'center')
						 :attr('data-sort-value', spell.name)
			row:tag('td'):wikitext(Icons.Icon({spell.name, type=spellBook.imgType, noicon=true}))

			if includeTypeColumn then
				row:tag('td'):wikitext(p.getSpellTypeLink(spellBook.id))
							 :attr('data-sort-value', spellBook.id)
			end

			row:tag('td'):wikitext(p._getSpellRequirements(spell))
					     :attr('data-sort-value', (spell.abyssalLevel or spell.level))
			row:tag('td'):wikitext(Icons.getDLCColumnIcon(spell.id))
						 :attr('data-sort-value', Icons.getExpansionID(spell.id))
						 :css('text-align', 'center')
						 
			--11/01/22: Added base damage if requested
			if includeDamage then
				local dmg = p._getSpellStat(spell, 'spellDamage')
				if dmg > 0 then
					row:tag('td'):wikitext(dmg)
								 :css('text-align', 'right')
				else
					row:tag('td'):wikitext('N/A')
								 :addClass('table-na')
				end
			end
			
			--8/20/21: Changed to just getting the spell's description outright
			row:tag('td'):wikitext(p._getSpellStat(spell, 'description'))

			--1/4/22: haha just kidding. Now we're also getting delay between attacks for spells with special attacks
			--25/06/2024: I accidentally fixed this with a refactor and it messes up the table because it has been broken for a long time.
			--		      So I commented it out.
			--local spAttID = spell.specialAttackID or spell.specialAttack
			--if spAttID ~= nil then
			--	local spAtt = Attacks.getAttackByID(spAttID)
			--	local interval = spAtt.attackInterval
			--	local hits = spAtt.attackCount ~= nil and spAtt.attackCount or 1
			--	if interval ~= nil and hits > 1 then
			--		local intervalTable = {}
			--		table.insert(intervalTable, '<br/>(' .. Num.round(interval / 1000, 2, 2) .. 's delay between attacks.')
			--		if hits > 2 then
			--			table.insert(intervalTable, ' ' .. Num.round(interval * (hits - 1) / 1000, 2, 2) .. 's total duration.')
			--		end
			--		table.insert(intervalTable, ')')
			--		row:tag('td'):wikitext(table.concat(intervalTable))
			--	end
			--end
			if includeExperience then
				local xp = spell.baseExperience
				if xp == nil or xp == 0 then
					row:tag('td'):wikitext('N/A')
								 :addClass('table-na')
				else
					row:tag('td'):wikitext(xp)
								 :addClass('text-align', 'right')
				end
			end
			row:tag('td'):wikitext(p._getSpellRunes(spell))
						 :css('text-align', 'center')
						 :css('white-space', 'nowrap')
			if includeItems then
				row:tag('td'):wikitext(p._getSpellItems(spell))
				        	 :css('text-align', 'center')
			end
		end

		return tostring(html)
	end
end

function p.getSpellTableFromList(frame)
	local args = frame.args ~= nil and frame.args or frame
	local spellListText = args[1]
	local includeSpellbook = args.includeSpellbook ~= nil and string.lower(args.includeSpellbook) == 'true'
	local spellNames = Shared.splitString(spellListText, ',')
	local spellList = {}
	for i, spellName in ipairs(spellNames) do
		local spell = p.getSpell(spellName)
		if spell == nil then
			return Shared.printError('No spell named "' .. spellName .. '" exists in the data module')
		else
			table.insert(spellList, spell)
		end
	end
	return p._getSpellTable(spellList, includeSpellbook)
end

function p.getSpellBookTable(frame)
	local spellBook = frame.args ~= nil and frame.args[1] or frame[1]
	spellBook = p.getSpellBookID(spellBook)
	return p._getSpellTable(p.getSpellsBySpellBook(spellBook), false)
end

return p