Module:Magic: Difference between revisions

From Melvor Idle
(Attempt to resolve issue with spell names containing apostrophes)
(Add damage column for abyssal spell tables, add initial combat effects handling)
 
(74 intermediate revisions by 4 users not shown)
Line 1: Line 1:
local p = {}
local p = {}


local MagicData = mw.loadData('Module:Magic/data')
local Areas = require('Module:CombatAreas')
local Shared = require('Module:Shared')
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 Icons = require('Module:Icons')
local Items = require('Module:Items')
local Items = require('Module:Items')
local Constants = require('Module:Constants')
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 processSpell(section, index)
function p.getSpellByProperty(spellProperty, propertyName, spellType)
  local result = Shared.clone(MagicData[section][index])
if spellType == nil then
  result.id = index - 1
-- Look for spell in all spellbooks
  result.type = section
for _, spellBook in ipairs(p.spellBooks) do
  return result
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
end


function p.getSpell(name, type)
--Returns the expansion icon for the spell if it has one
  local section = type
function p.getExpansionIcon(frame)
  if type == 'Spell' or type == 'Standard' then
local spellName = frame.args ~= nil and frame.args[1] or frame
    section = 'Spells'
local spell = p.getSpell(spellName)
  elseif type == 'Curse' then
if spell == nil then
    section = 'Curses'
return Shared.printError('No spell named "' .. spellName .. '" exists in the data module')
  elseif type == 'Aurora' then
end
    section = 'Auroras'
 
  elseif type == 'Alt Magic' or type == 'Alternative Magic' then
return Icons.getExpansionIcon(spell.id)
    section='AltMagic'
  end
 
  name = string.gsub(name, "'", "'")
  if section == nil then
    for i, section in Shared.skpairs(MagicData) do
      for j, spell in Shared.skpairs(section) do
        if spell.name == name then
          return processSpell(i, j)
        end
      end
    end
  elseif section ~= nil and MagicData[section] ~= nil then
    for i, spell in Shared.skpairs(MagicData[section]) do
      if spell.name == name then
        return processSpell(section, i)
      end
    end
  else
    return nil
  end
end
end


function p.getSpellByID(type, id)
function p._getSpellIconType(spell)
  local section = type
local spellBook = p.getSpellBookFromSpell(spell)
  if type == nil or type == 'Spell' or type == 'Standard' then
if spellBook == nil then
    section = 'Spells'
-- Pick a suitable default
  elseif type == 'Curse' then
return 'spell'
    section = 'Curses'
else
  elseif type == 'Aurora' then
return spellBook.imgType
    section = 'Auroras'
end
  elseif type == 'Alt Magic' or type == 'Alternative Magic' then
    section='AltMagic'
  end
 
  if MagicData[section] ~= nil then
    return processSpell(section, id + 1)
  else
    return nil
  end
end
end


function p.getTypeString(type)
function p.getSpellIconType(frame)
  if type == 'Auroras' then
local spellName = frame.args ~= nil and frame.args[1] or frame
    return 'Aurora'
local spell = p.getSpell(spellName)
  elseif type == 'Curses' then
if spell == nil then
    return 'Curse'
return 'spell'
  elseif type == 'AltMagic' then
else
    return 'Alt. Magic'
return p._getSpellIconType(spell)
  elseif type == 'Spells' then
end
    return "Combat Spell"
  elseif type == 'Ancient' then
    return 'Ancient Magick'
  end
end
end


function p._getSpellIcon(spell, size)
function p._getSpellIcon(spell, size)
  if size == nil then size = 50 end
if size == nil then size = 50 end
  if spell.type == 'Auroras' then
local imgType = p._getSpellIconType(spell)
    return Icons.Icon({spell.name, type='aurora', notext=true, size=size})
return Icons.Icon({spell.name, type=imgType, notext=true, size=size})
  elseif spell.type == 'Curses' then
    return Icons.Icon({spell.name, type='curse', notext=true, size=size})
  else
    return Icons.Icon({spell.name, type='spell', notext=true, size=size})
  end
end
end


function p._getSpellRequirements(spell)
function p._getSpellRequirements(spell)
  local result = ''
-- All spells have a Magic level requirement
  result = result..Icons._SkillReq('Magic', spell.level)
local extraReqs = {}
  if spell.requiredItem ~= nil and spell.requiredItem >= 0 then
if spell.abyssalLevel ~= nil and spell.abyssalLevel > 0 then
    local reqItem = Items.getItemByID(spell.requiredItem)
table.insert(extraReqs, {
    result = result..'<br/>'..Icons.Icon({reqItem.name, type='item', notext=true})..' equipped'
['type'] = 'AbyssalLevel',
  end
['skillID'] = 'melvorD:Magic',
  if spell.requiredDungeonCompletion ~= nil then
['level'] = spell.abyssalLevel
    local dung = Areas.getAreaByID('dungeon', spell.requiredDungeonCompletion[1])
})
    result = result..'<br/>'..Icons.Icon({dung.name, type='dungeon', notext=true, qty=spell.requiredDungeonCompletion[2]})..' Clears'
else
  end
table.insert(extraReqs, {
  return result
['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 sub = mw.html.create('sub')
:wikitext(req.quantity)
:css('vertical-align', 'super')
:css('font-size', 'smaller')
:css('margin-right', '1px')
:css('margin-left', '3px')
:done()
html:node(sub)
html:wikitext(Icons.Icon({rune.name, type='item', notext=true}))
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
end


function p._getSpellRunes(spell)
function p._getSpellRunes(spell)
  local result = ''
if type(spell.runesRequired) == 'table' then
  for i, req in Shared.skpairs(spell.runesRequired) do
local resultPart  = {}
    local rune = Items.getItemByID(req.id)
table.insert(resultPart, formatRuneList(spell.runesRequired))
    if i > 1 then result = result..', ' end
if spell.runesRequiredAlt ~= nil and not Shared.tablesEqual(spell.runesRequired, spell.runesRequiredAlt) then
    result = result..Icons.Icon({rune.name, type='item', notext=true, qty=req.qty})
table.insert(resultPart, "<br/>'''OR'''<br/>" .. formatRuneList(spell.runesRequiredAlt))
  end
end
  if spell.runesRequiredAlt ~= nil and not Shared.tablesEqual(spell.runesRequired, spell.runesRequiredAlt) then
return table.concat(resultPart)
    result = result.."<br/>'''OR'''<br/>"
else
    for i, req in pairs(spell.runesRequiredAlt) do
return ''
      local rune = Items.getItemByID(req.id)
end
      if i > 1 then result = result..', ' end
      result = result..Icons.Icon({rune.name, type='item', notext=true, qty=req.qty})
    end
  end
  return result
end
end


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


function p._getSpellDescription(spell)
-- Generates description template data. See: altMagic.js, description()
  local result = ''
function p._getSpellTemplateData(spell)
  if spell.description ~= nil then
local templateData = nil
    if p.getSpellTypeIndex(spell.type) == 4 and string.find(spell.description, "<br>") ~= nil then
local spellBook = p.getSpellBookFromSpell(spell)
      result = string.sub(spell.description, 0, string.find(spell.description, "<br>")-1)
if spellBook.id == 'altMagic' then
    else
if spell.produces ~= nil then
      result = spell.description
-- Item produced varies depending on items consumed
    end
if spell.produces == 'Bar' then
  elseif spell.modifiers ~= nil then
templateData = {
    result = Constants.getModifiersText(spell.modifiers, false)
["barAmount"] = spell.productionRatio,
  elseif spell.type == 'Spells' then
["oreAmount"] = spell.specialCost.quantity
    result = 'Combat spell with a max hit of '..(spell.maxHit * 10)
}
  end
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


  return result
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
end


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


function p.getSpellStat(frame)
function p.getSpellStat(frame)
  local spellName = frame.args ~= nil and frame.args[1] or frame[1]
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 statName = frame.args ~= nil and frame.args[2] or frame[2]
  local spell = p.getSpell(spellName)
local spell = p.getSpell(spellName)
  if spell == nil then
if spell == nil then
    return "ERROR: No spell named "..spellName.." found"
return Shared.printError('No spell named "' .. spellName .. '" exists in the data module')
  end
end
  return p._getSpellStat(spell, statName)
return p._getSpellStat(spell, statName)
end
end


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


  local result = ''
local result = ''


  --8/20/21: Changed to using the new getSpellDescription function
--11/01/22: Added Spell Damage for standard & archaic spells
  result = result.."\r\n|-\r\n|'''Description:'''<br/>"..p._getSpellStat(spell, 'description')
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
return result
end
end


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


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


function p.getSpellsForRune(runeID)
function p._getAltSpellCostText(spell)
  local spellList = {}
if spell.specialCost ~= nil then
  for secName, secArray in Shared.skpairs(MagicData) do
local costType = spell.specialCost.type
    for i, spell in pairs(secArray) do
if costType == nil or costType == 'None' then
      local foundSpell = false
if type(spell.fixedItemCosts) == 'table' then
      for j, req in pairs(spell.runesRequired) do
local costText = {}
        if req.id == runeID then
for i, itemCost in ipairs(spell.fixedItemCosts) do
          table.insert(spellList, processSpell(secName, i))
local item = Items.getItemByID(itemCost.id)
          foundSpell = true
if item ~= nil then
          break
table.insert(costText, Icons.Icon({item.name, type='item', qty=itemCost.quantity}))
        end
end
      end
end
      if spell.runesRequiredAlt ~= nil and not foundSpell then
if not Shared.tableIsEmpty(costText) then
        for j, req in pairs(spell.runesRequiredAlt) do
return table.concat(costText, ', ')
          if req.id == runeID then
end
            table.insert(spellList, processSpell(secName, i))
else
            break
return nil
          end
end
        end
else
      end
local qty = Num.formatnum(spell.specialCost.quantity)
    end
local typeString = {
  end
['AnyItem'] = qty .. ' of any item',
  table.sort(spellList, function(a, b)  
['BarIngredientsWithCoal'] = qty .. ' x required ores for the chosen bar',
              if a.type ~= b.type then
['BarIngredientsWithoutCoal'] = qty .. ' x required ores (except ' .. Icons.Icon({'Coal Ore', type='item'}) .. ') for the chosen bar',
                return p.getSpellTypeIndex(a.type) < p.getSpellTypeIndex(b.type)
['JunkItem'] = qty .. ' of any [[Fishing#Junk|Junk]] item',
              else
['SuperiorGem'] = qty .. ' of any superior gem',
                return a.level < b.level
['AnyNormalFood'] = qty .. ' x non-perfect food'
              end
}
            end)
return typeString[costType]
  return spellList
end
end
end
end


function p.getSpellTypeIndex(type)
function p.getSpellsProducingItem(itemID)
  if type == 'Spells' then
-- Only need to check Alt. Magic spells
    return 0
local spellList = {}
  elseif type == 'Curses' then
 
    return 1
-- Classify whether the item fits into various categories
  elseif type == 'Auroras' then
local isBar, isShard, isGem, isSuperiorGem, isPerfectFood = false, false, false, false, false
    return 2
local item = Items.getItemByID(itemID)
  elseif type == 'Ancient' then
if item ~= nil then
    return 3
isBar = not Shared.tableIsEmpty(GameData.getEntities(SkillData.Smithing.recipes,
  elseif type == 'AltMagic' then
function(recipe)
    return 4
return recipe.categoryID == 'melvorD:Bars' and recipe.productID == item.id
  end
end))
  return -1
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
end


function p.getSpellTypeLink(type)
-- If includeConsumes = true, then checks for Alt. Magic spell resource consumptions as well as
  if type == 'Spells' then
-- the rune cost of spells
    return Icons.Icon({'Magic', 'Standard', img='Standard', type='spellType'})
function p.getSpellsUsingItem(itemID, includeConsumes)
  elseif type == 'Curses' then
if type(includeConsumes) ~= 'boolean' then
    return Icons.Icon({'Curses', 'Curse', img='Curse', type='spellType'})
includeConsumes = false
  elseif type == 'Auroras' then
end
    return Icons.Icon({'Auroras', 'Aurora', img='Aurora', type='spellType'})
local runeKeys = { 'runesRequired', 'runesRequiredAlt' }
  elseif type == 'Ancient' then
local spellList = {}
    return Icons.Icon({'Ancient Magicks', 'Ancient', img='Ancient', type='spellType'})
  elseif type == 'AltMagic' then
-- Initialize some vars & only populate if we're including resource consumptions
    return Icons.Icon({'Alt. Magic', type='skill'})
local isJunkItem, isSuperiorGem, isNormalFood, isCoal, isBarIngredient = false, false, false, false, false
  end
if includeConsumes then
  return ''
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
end


function p._getSpellRow(spell, includeTypeColumn)
-- The below function is included for backwards compatibility
  local rowTxt = '\r\n|-\r\n|data-sort-value="'..spell.name..'"|'
function p.getSpellsForRune(runeID)
  if spell.type == 'Auroras' then
return p.getSpellsUsingItem(runeID, false)
    rowTxt = rowTxt..Icons.Icon({spell.name, type='aurora', notext=true, size=50})
end
  elseif spell.type == 'Curses' then
    rowTxt = rowTxt..Icons.Icon({spell.name, type='curse', notext=true, size=50})
  else
    rowTxt = rowTxt..Icons.Icon({spell.name, type='spell', notext=true, size=50})
  end
  rowTxt = rowTxt..'||'..Icons.Icon({spell.name, type='spell', noicon=true})
  rowTxt = rowTxt..'||data-sort-value="'..spell.level..'"|'..Icons._SkillReq('Magic', spell.level)
  --Handle required items/dungeon clears
  if spell.requiredItem ~= nil and spell.requiredItem >= 0 then
    local reqItem = Items.getItemByID(spell.requiredItem)
    rowTxt = rowTxt..'<br/>'..Icons.Icon({reqItem.name, type='item', notext=true})..' equipped'
  end
  if spell.requiredDungeonCompletion ~= nil then
    local dung = Areas.getAreaByID('dungeon', spell.requiredDungeonCompletion[1])
    rowTxt = rowTxt..'<br/>'..Icons.Icon({dung.name, type='dungeon', notext=true, qty=spell.requiredDungeonCompletion[2]})..' Clears'
  end


  if includeTypeColumn then
function p.getSpellTypeLink(spellBookID)
    rowTxt = rowTxt..'||data-sort-value="'..p.getSpellTypeIndex(spell.type)..'"|'
if spellBookID == 'standard' then
    rowTxt = rowTxt..p.getSpellTypeLink(spell.type)
return Icons.Icon({'Standard Magic', 'Standard', img='Standard', type='spellType'})
  end
elseif spellBookID == 'ancient' then
  --8/20/21: Changed to just getting the spell's description outright
return Icons.Icon({'Ancient Magicks', 'Ancient', img='Ancient', type='spellType'})
  rowTxt = rowTxt..'||'..p._getSpellStat(spell, 'description')
elseif spellBookID == 'archaic' then
  if p.getSpellTypeIndex(spell.type) == 4 then
return Icons.Icon({'Archaic Magicks', 'Archaic', img='Archaic', type='spellType'})
    rowTxt = rowTxt..'||'..spell.magicXP
elseif spellBookID == 'abyssal' then
  end
return Icons.Icon({'Abyssal Magicks', 'Abyssal', img='Abyssal', type='spellType'})
  rowTxt = rowTxt..'||style="text-align:center"|'
elseif spellBookID == 'curse' then
  rowTxt = rowTxt..p._getSpellRunes(spell)
return Icons.Icon({'Curses', 'Curse', img='Curse', type='spellType'})
  return rowTxt
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
end


function p.getStandardSpellsTable(frame)
function p._getSpellHeader(includeTypeColumn, includeItems, includeDamage, includeExperience)
  local result = '{|class="wikitable sortable"\r\n!colspan="2"|Spell'
 
  result = result..'!!Requirements'
  result = result..'!!style="width:275px"|Description'
  result = result..'!!Runes'
  local spellList = {}
  for i, spell in Shared.skpairs(MagicData.Spells) do
    local rowTxt = p._getSpellRow(processSpell('Spells', i), false)
    result = result..rowTxt
  end
  result = result..'\r\n|}'
  return result
end
end


function p.getCurseTable(frame)
function p._getSpellRow(spell, includeTypeColumn, includeItems, includeDamage, includeExperience)
  local result = '{|class="wikitable sortable"\r\n!colspan="2"|Spell'
 
  result = result..'!!Requirements'
  result = result..'!!style="width:275px"|Description'
  result = result..'!!Runes'
  local spellList = {}
  for i, spell in Shared.skpairs(MagicData.Curses) do
    local rowTxt = p._getSpellRow(processSpell('Curses', i), false)
    result = result..rowTxt
  end
  result = result..'\r\n|}'
  return result
end
end


function p.getAuroraTable(frame)
function p._getSpellTable(spellList, includeTypeColumn)
  local result = '{|class="wikitable sortable"\r\n!colspan="2"|Spell'
if type(spellList) == 'table' and not Shared.tableIsEmpty(spellList) then
  result = result..'!!Requirements'
local includeSpellbook, includeItems, includeDamage, includeExperience = false, false, false, false
  result = result..'!!style="width:275px"|Description'
if type(includeTypeColumn) == 'boolean' then
  result = result..'!!Runes'
includeSpellbook = includeTypeColumn
  for i, spell in Shared.skpairs(MagicData.Auroras) do
end
    local rowTxt = p._getSpellRow(processSpell('Auroras', i), false)
-- Check to see what columns are required
    result = result..rowTxt
for i, spell in ipairs(spellList) do
  end
local spellBook = p.getSpellBookFromSpell(spell)
  result = result..'\r\n|}'
if not includeItems and p._getSpellItems(spell) ~= '' then
  return result
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
end


function p.getAncientTable(frame)
function p.getSpellTableFromList(frame)
  local result = '{|class="wikitable sortable"\r\n!colspan="2"|Spell'
local args = frame.args ~= nil and frame.args or frame
  result = result..'!!Requirements'
local spellListText = args[1]
  result = result..'!!style="width:275px"|Description'
local includeSpellbook = args.includeSpellbook ~= nil and string.lower(args.includeSpellbook) == 'true'
  result = result..'!!Runes'
local spellNames = Shared.splitString(spellListText, ',')
  for i, spell in Shared.skpairs(MagicData.Ancient) do
local spellList = {}
    local rowTxt = p._getSpellRow(processSpell('Ancient', i), false)
for i, spellName in ipairs(spellNames) do
    result = result..rowTxt
local spell = p.getSpell(spellName)
  end
if spell == nil then
  result = result..'\r\n|}'
return Shared.printError('No spell named "' .. spellName .. '" exists in the data module')
  return result
else
table.insert(spellList, spell)
end
end
return p._getSpellTable(spellList, includeSpellbook)
end
end


function p.getAltSpellsTable(frame)
function p.getSpellBookTable(frame)
  local result = '{|class="wikitable sortable"\r\n!colspan="2"|Spell'
local spellBook = frame.args ~= nil and frame.args[1] or frame[1]
  result = result..'!!Requirements'
spellBook = p.getSpellBookID(spellBook)
  result = result..'!!style="width:275px"|Description'
return p._getSpellTable(p.getSpellsBySpellBook(spellBook), false)
  result = result..'!!Experience'
  result = result..'!!Runes'
  local spellList = {}
  for i, spell in Shared.skpairs(MagicData.AltMagic) do
    local rowTxt = p._getSpellRow(processSpell('AltMagic', i), false)
    result = result..rowTxt
  end
  result = result..'\r\n|}'
  return result
end
end


return p
return p

Latest revision as of 11:40, 6 September 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 sub = mw.html.create('sub')
				:wikitext(req.quantity)
				:css('vertical-align', 'super')
				:css('font-size', 'smaller')
				:css('margin-right', '1px')
				:css('margin-left', '3px')
				:done()
			html:node(sub)
			html:wikitext(Icons.Icon({rune.name, type='item', notext=true}))
		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