Module:Magic: Difference between revisions

From Melvor Idle
(getSpellsProducingItem: Remove erroneous break)
(Update for v1.3)
 
(22 intermediate revisions by 2 users not shown)
Line 1: Line 1:
local p = {}
local p = {}


local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
local GameData = require('Module:GameData')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local SkillData = GameData.skillData
local Common = require('Module:Common')
local Modifiers = require('Module:Modifiers')
local Attacks = require('Module:Attacks')
local Attacks = require('Module:Attacks')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
Line 10: Line 11:


p.spellBooks = {
p.spellBooks = {
    { id = 'standard', dataID = 'standardSpells', name = 'Standard Magic', imgType = 'spell' },
{ id = 'standard', dataID = 'attackSpells', name = 'Standard Magic', imgType = 'spell', bookID = 'melvorD:Standard' },
    { id = 'ancient', dataID = 'ancientSpells', name = 'Ancient Magick', imgType = 'spell' },
{ id = 'ancient', dataID = 'attackSpells', name = 'Ancient Magick', imgType = 'spell', bookID = 'melvorF:Ancient' },
    { id = 'archaic', dataID = 'archaicSpells', name = 'Archaic Magick', imgType = 'spell' },
{ id = 'archaic', dataID = 'attackSpells', name = 'Archaic Magick', imgType = 'spell', bookID = 'melvorTotH:Archaic' },
    { id = 'curse', dataID = 'curseSpells', name = 'Curse', imgType = 'curse' },
{ id = 'abyssal' , dataID = 'attackSpells', name = 'Abyssal', imgType = 'spell', bookID = 'melvorItA:Abyssal' },
    { id = 'aurora', dataID = 'auroraSpells', name = 'Aurora', imgType = 'aurora' },
{ id = 'curse', dataID = 'curseSpells', name = 'Curse', imgType = 'curse' },
    { id = 'altMagic', dataID = 'altSpells', name = 'Alt. Magic', imgType = 'spell', dataBySkill = true }
{ id = 'aurora', dataID = 'auroraSpells', name = 'Aurora', imgType = 'aurora' },
{ id = 'altMagic', dataID = 'altSpells', name = 'Alt. Magic', imgType = 'spell', dataRoot = GameData.getSkillData('melvorD:Magic') }
}
}
p.spellBookIndex = {}
for i, spellBook in ipairs(p.spellBooks) do
    p.spellBookIndex[spellBook.id] = i
end


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


-- Retrieves all spells within the given spellbook
-- Retrieves all spells within the given spellbook
function p.getSpellsBySpellBook(spellBookID)
function p.getSpellsBySpellBook(spellBookID)
    if type(spellBookID) == 'string' then
if type(spellBookID) == 'string' then
        local spellBook = GameData.getEntityByID(p.spellBooks, spellBookID)
local spellBook = GameData.getEntityByID(p.spellBooks, spellBookID)
        if spellBook ~= nil then
if spellBook ~= nil then
            if spellBook.dataBySkill then
local dataRoot = spellBook.dataRoot or GameData.rawData
                -- Data is part of the Magic skill object
local spellData = dataRoot[spellBook.dataID]
                local magicData = GameData.getSkillData('melvorD:Magic')
if spellBook.bookID == nil then
                if magicData ~= nil then
return spellData
                    return magicData[spellBook.dataID]
else
                end
return GameData.getEntities(spellData, function(spell) return spell.spellbook == spellBook.bookID end)
            else
end
                -- Data is at the root of GameData
end
                return GameData.rawData[spellBook.dataID]
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
end


function p.getSpell(name, spellType)
function p.getSpell(name, spellType)
local spellBookID = p.getSpellBookID(spellType)
return p.getSpellByProperty(Shared.fixPagename(name), 'name', spellType)
name = Shared.fixPagename(name)
end
 
function p.getSpellByID(spellID, spellType)
return p.getSpellByProperty(spellID, 'id', spellType)
end


    for i, spellBook in ipairs(p.spellBooks) do
function p.getSpellByProperty(spellProperty, propertyName, spellType)
        if spellBookID == nil or spellBookID == spellBook.id then
if spellType == nil then
            local spells = p.getSpellsBySpellBook(spellBook.id)
-- Look for spell in all spellbooks
            local spell = GameData.getEntityByName(spells, name)
for _, spellBook in ipairs(p.spellBooks) do
            if spell ~= nil then
local spells = p.getSpellsBySpellBook(spellBook.id)
                return spell
if spells ~= nil and not Shared.tableIsEmpty(spells) then
            end
local spell = GameData.getEntityByProperty(spells, propertyName, spellProperty)
        end
if spell ~= nil then
    end
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.getSpellByID(id, spellType)
--Returns the expansion icon for the spell if it has one
local spellBookID = p.getSpellBookID(spellType)
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


    if spellType == nil or spellBookID ~= nil then
return Icons.getExpansionIcon(spell.id)
        for i, spellBook in ipairs(p.spellBooks) do
            if spellType == nil or spellBookID == spellBook.id then
                if spellBook.dataBySkill then
                    return GameData.getEntityByID(p.getSpellsBySpellBook(spellBook.id), id)
                else
                    return GameData.getEntityByID(spellBook.dataID, id)
                end
            end
        end
    end
end
end


function p.getTypeString(spellType)
function p._getSpellIconType(spell)
    local spellBookID = p.getSpellBookID(spellType)
local spellBook = p.getSpellBookFromSpell(spell)
    if spellBookID ~= nil then
if spellBook == nil then
        local spellBook = GameData.getEntityByID(p.spellBooks, spellBookID)
-- Pick a suitable default
        if spellBook ~= nil then
return 'spell'
            return spellBook.name
else
        end
return spellBook.imgType
    end
end
end
end


function p._getSpellIconType(spell)
function p.getSpellIconType(frame)
    local spellBook = GameData.getEntityByID(p.spellBooks, spell.spellBook)
local spellName = frame.args ~= nil and frame.args[1] or frame
    if spellBook == nil then
local spell = p.getSpell(spellName)
        -- Pick a suitable default
if spell == nil then
        return 'spell'
return 'spell'
    else
else
        return spellBook.imgType
return p._getSpellIconType(spell)
    end
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
    local imgType = p._getSpellIconType(spell)
local imgType = p._getSpellIconType(spell)
    return Icons.Icon({spell.name, type=imgType, notext=true, size=size})
return Icons.Icon({spell.name, type=imgType, notext=true, size=size})
end
end


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


    if spell.itemRequiredID ~= nil then
local resultPart = {}
        local item = Items.getItemByID(spell.itemRequiredID)
for i, reqs in ipairs({ extraReqs, spell.requirements }) do
        if item ~= nil then
local reqStr = Common.getRequirementString(reqs)
            table.insert(resultPart, Icons.Icon({item.name, type='item', notext=true}) .. ' Equipped')
if reqStr ~= nil then
        end
table.insert(resultPart, reqStr)
    end
end
    if spell.requirements ~= nil then
end
        for i, req in ipairs(spell.requirements) do
            if req.type == 'DungeonCompletion' then
                local dung = GameData.getEntityByID('dungeons', req.dungeonID)
                if dung ~= nil then
                    table.insert(resultPart, Icons.Icon({dung.name, type='dungeon', qty=req.count, notext=true}) .. ' Clears')
                end
            elseif req.type == 'MonsterKilled' then
                local monster = GameData.getEntityByID('monsters', req.monsterID)
                if monster ~= nil then
                    table.insert(resultPart, Icons.Icon({monster.name, type='monster', qty=req.count, notext=true}) .. ' Kills')
                end
            else
                table.insert(resultPart, 'ERROR: Unknown requirement: ' .. (req.type or 'nil') .. '[[Category:Pages with script errors]]')
            end
        end
    end


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


local function formatRuneList(runes)
local function formatRuneList(runes)
    local runeList = {}
local runeList = {}
    for i, req in ipairs(runes) do
for i, req in ipairs(runes) do
        local rune = Items.getItemByID(req.id)
local rune = Items.getItemByID(req.id)
        if rune ~= nil then
if rune ~= nil then
            table.insert(runeList, Icons.Icon({rune.name, type='item', notext=true, qty=req.quantity}))
table.insert(runeList, Icons.Icon({rune.name, type='item', notext=true, qty=req.quantity}))
        end
end
    end
end
    return table.concat(runeList, ', ')  
return table.concat(runeList, ', ')
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


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


function p._getSpellDescription(spell)
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
if spell.description ~= nil then
        return Shared.applyTemplateData(spell.description, p._getSpellTemplateData(spell))
return Shared.applyTemplateData(spell.description, p._getSpellTemplateData(spell))
    elseif spell.modifiers ~= nil or spell.targetModifiers ~= nil then
elseif spell.modifiers ~= nil or spell.targetModifiers ~= nil then
        local resultPart = {}
local resultPart = {}
        if spell.modifiers ~= nil then
if spell.modifiers ~= nil then
            table.insert(resultPart, Constants.getModifiersText(spell.modifiers, false))
table.insert(resultPart, Modifiers.getModifiersText(spell.modifiers, false, inline))
        end
end
        if spell.targetModifiers ~= nil then
if spell.targetModifiers ~= nil then
            local targetModText = Constants.getModifiersText(spell.targetModifiers, false)
local targetModText = Modifiers.getModifiersText(spell.targetModifiers, false, inline)
            table.insert(resultPart, 'Enemies are inflicted with:<br/>' .. targetModText)
if inline then
        end
table.insert(resultPart, targetModText)
        return table.concat(resultPart, '<br/>')
else
    elseif spell.specialAttackID ~= nil then
table.insert(resultPart, 'Enemies are inflicted with:<br/>' .. targetModText)
    local spAtt = Attacks.getAttackByID(spell.specialAttackID)
end
    if spAtt ~= nil then
end
    return spAtt.description
return table.concat(resultPart, connector)
    end
elseif spell.specialAttackID ~= nil or spell.specialAttack ~= nil then
    elseif spell.spellBook == 'standard' then
local spAtt = Attacks.getAttackByID(spell.specialAttackID or spell.specialAttack)
        return 'Combat spell with a max hit of ' .. Shared.formatnum(spell.maxHit * 10)
if spAtt ~= nil then
    else
return spAtt.description
        return ''
end
    end
elseif spellBook.id == 'standard' then
return 'Combat spell with a max hit of ' .. Shared.formatnum(spell.maxHit * 10)
else
return ''
end
end
end


Line 268: Line 325:
return p._getSpellRunes(spell)
return p._getSpellRunes(spell)
elseif stat == 'type' then
elseif stat == 'type' then
return p.getTypeString(spell.spellBook)
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
end
return spell[stat]
return spell[stat]
Line 278: Line 342:
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)
Line 287: Line 351:
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 = ''


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


function p._getSpellCategories(spell)
function p._getSpellCategories(spell)
local spellBook = p.getSpellBookFromSpell(spell)
local result = '[[Category:Spells]]'
local result = '[[Category:Spells]]'
result = result..'[[Category:'..p.getTypeString(spell.spellBook)..']]'
result = result..'[[Category:' .. spellBook.name .. ']]'
return result
return result
end
end
Line 308: Line 382:
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)
Line 314: Line 388:


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


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


    -- Classify whether the item fits into various categories
-- Classify whether the item fits into various categories
    local isBar, isShard, isGem, isSuperiorGem, isPerfectFood = false, false, false, false, false
local isBar, isShard, isGem, isSuperiorGem, isPerfectFood = false, false, false, false, false
    local item = Items.getItemByID(itemID)
local item = Items.getItemByID(itemID)
    if item ~= nil then
if item ~= nil then
        isBar = item.type == 'Bar'
isBar = not Shared.tableIsEmpty(GameData.getEntities(SkillData.Smithing.recipes,
        isShard = GameData.getEntityByProperty(SkillData.Magic.randomShards, 'itemID', item.id) ~= nil
function(recipe)
        isGem = GameData.getEntityByProperty('randomGems', 'itemID', itemID) ~= nil
return recipe.categoryID == 'melvorD:Bars' and recipe.productID == item.id
        isSuperiorGem = item.type == 'Superior Gem'
end))
        if item.healsFor ~= nil then
isShard = GameData.getEntityByProperty(SkillData.Magic.randomShards, 'itemID', item.id) ~= nil
            -- Item is food, but is it a product of perfect cooking?
isGem = GameData.getEntityByProperty('randomGems', 'itemID', itemID) ~= nil
            local cookData = GameData.getSkillData('melvorD:Cooking')
--Runestone can't be created by Alt Magic spells that make random superior gems.
            if cookData ~= nil and cookData.recipes ~= nil then
isSuperiorGem = item.type == 'Superior Gem' and item.id ~= SkillData.Mining.runestoneItemID
                isPerfectFood = GameData.getEntityByProperty(cookData.recipes, 'perfectCookID', itemID) ~= nil
if item.healsFor ~= nil then
            end
-- Item is food, but is it a product of perfect cooking?
        end
local cookData = GameData.getSkillData('melvorD:Cooking')
    end
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
for i, spell in ipairs(p.getSpellsBySpellBook('altMagic')) do
        local includeSpell = false
local includeSpell = false
        if spell.produces ~= nil then
if spell.produces ~= nil then
            if spell.produces == itemID then
if spell.produces == itemID then
                includeSpell = true
includeSpell = true
            else
else
                includeSpell = ((isBar and spell.produces == 'Bar') or
includeSpell = ((isBar and spell.produces == 'Bar') or
                    (isShard and spell.produces == 'RandomShards') or
(isShard and spell.produces == 'RandomShards') or
                    (isGem and spell.produces == 'RandomGem') or
(isGem and spell.produces == 'RandomGem') or
                    (isSuperiorGem and spell.produces == 'RandomSuperiorGem') or
(isSuperiorGem and spell.produces == 'RandomSuperiorGem') or
                    (isPerfectFood and spell.produces == 'PerfectFood'))
(isPerfectFood and spell.produces == 'PerfectFood'))
            end
end
            if includeSpell then
if includeSpell then
                table.insert(spellList, spell)
table.insert(spellList, spell)
            end
end
        end
end
    end
end


    table.sort(spellList, function(a, b) return a.level < b.level end)
table.sort(spellList, function(a, b) return a.level < b.level end)
return spellList
return spellList
end
end
Line 395: Line 473:
includeConsumes = false
includeConsumes = false
end
end
    local runeKeys = { 'runesRequired', 'runesRequiredAlt' }
local runeKeys = { 'runesRequired', 'runesRequiredAlt' }
    local spellList = {}
local spellList = {}
   
    -- Initialize some vars & only populate if we're including resource consumptions
-- Initialize some vars & only populate if we're including resource consumptions
    local isJunkItem, isSuperiorGem, isNormalFood, isCoal, isBarIngredient = false, false, false, false, false
local isJunkItem, isSuperiorGem, isNormalFood, isCoal, isBarIngredient = false, false, false, false, false
    if includeConsumes then
if includeConsumes then
        local thisItem = Items.getItemByID(itemID)
local thisItem = Items.getItemByID(itemID)
        local junkItemIDs = GameData.getSkillData('melvorD:Fishing').junkItemIDs
local junkItemIDs = GameData.getSkillData('melvorD:Fishing').junkItemIDs
        isJunkItem = Shared.contains(junkItemIDs, itemID)
isJunkItem = Shared.contains(junkItemIDs, itemID)
        isSuperiorGem = thisItem.type == 'Superior Gem'
isSuperiorGem = thisItem.type == 'Superior Gem'
        if thisItem.healsFor ~= nil then
if thisItem.healsFor ~= nil then
            -- Item is food, but is it from cooking & is it normal or perfect?
-- Item is food, but is it from cooking & is it normal or perfect?
            local cookData = GameData.getSkillData('melvorD:Cooking')
local cookData = GameData.getSkillData('melvorD:Cooking')
            if cookData ~= nil and cookData.recipes ~= nil then
if cookData ~= nil and cookData.recipes ~= nil then
                isNormalFood = GameData.getEntityByProperty(cookData.recipes, 'productID', itemID) ~= nil
isNormalFood = GameData.getEntityByProperty(cookData.recipes, 'productID', itemID) ~= nil
            end
end
        end
end
        isCoal = itemID == 'melvorD:Coal_Ore'
isCoal = itemID == 'melvorD:Coal_Ore'
        if not isCoal then
if not isCoal then
            -- Don't need to check if the item is another bar ingredient if we already know it is coal
-- 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
local smithingRecipes = GameData.getSkillData('melvorD:Smithing').recipes
            for i, recipe in ipairs(smithingRecipes) do
for i, recipe in ipairs(smithingRecipes) do
                if recipe.categoryID == 'melvorD:Bars' then
if recipe.categoryID == 'melvorD:Bars' then
                    for k, itemCost in ipairs(recipe.itemCosts) do
for k, itemCost in ipairs(recipe.itemCosts) do
                        if itemCost.id == itemID then
if itemCost.id == itemID then
                            isBarIngredient = true
isBarIngredient = true
                            break
break
                        end
end
                    end
end
                    if isBarIngredient then
if isBarIngredient then
                        break
break
                    end
end
                end
end
            end
end
        end
end
    end
end


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


    table.sort(spellList, function(a, b)
table.sort(spellList, function(a, b)
        if a.spellBook ~= b.spellBook then
local bookA, bookB = p.getSpellBookFromSpell(a), p.getSpellBookFromSpell(b)
            return p.spellBookIndex[a.spellBook] < p.spellBookIndex[b.spellBook]
if bookA.id ~= bookB.id then
        else
return bookA.id < bookB.id
            return a.level < b.level
else
        end
return a.level < b.level
    end)
end
end)
return spellList
return spellList
end
end
Line 494: Line 573:


function p.getSpellTypeLink(spellBookID)
function p.getSpellTypeLink(spellBookID)
    if spellBookID == 'standard' then
if spellBookID == 'standard' then
return Icons.Icon({'Standard Magic', 'Standard', img='Standard', type='spellType'})
return Icons.Icon({'Standard Magic', 'Standard', img='Standard', type='spellType'})
    elseif spellBookID == 'ancient' then
elseif spellBookID == 'ancient' then
        return Icons.Icon({'Ancient Magicks', 'Ancient', img='Ancient', type='spellType'})
return Icons.Icon({'Ancient Magicks', 'Ancient', img='Ancient', type='spellType'})
    elseif spellBookID == 'archaic' then
elseif spellBookID == 'archaic' then
        return Icons.Icon({'Archaic Magicks', 'Archaic', img='Archaic', type='spellType'})
return Icons.Icon({'Archaic Magicks', 'Archaic', img='Archaic', type='spellType'})
    elseif spellBookID == 'curse' then
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'})
return Icons.Icon({'Curses', 'Curse', img='Curse', type='spellType'})
    elseif spellBookID == 'aurora' then
elseif spellBookID == 'aurora' then
return Icons.Icon({'Auroras', 'Aurora', img='Aurora', type='spellType'})
return Icons.Icon({'Auroras', 'Aurora', img='Aurora', type='spellType'})
    elseif spellBookID == 'altMagic' then
elseif spellBookID == 'altMagic' then
return Icons.Icon({'Alt. Magic', type='skill'})
return Icons.Icon({'Alt. Magic', type='skill'})
end
end
Line 510: Line 591:
end
end


function p._getSpellRow(spell, includeTypeColumn)
function p._getSpellHeader(includeTypeColumn, includeItems, includeDamage, includeExperience)
    local spellBookIdx = p.spellBookIndex[spell.spellBook]
local resultPart = {}
    local spellBook = p.spellBooks[spellBookIdx]
table.insert(resultPart, '{|class="wikitable sortable"\n!colspan="2"| Spell')
    local rowPart = {'\r\n|-\r\n|data-sort-value="' .. spell.name .. '"| '}
if includeTypeColumn then
    table.insert(rowPart, Icons.Icon({spell.name, type=spellBook.imgType, notext=true, size=50}))
table.insert(resultPart, 'Spellbook')
table.insert(rowPart, '|| ' .. Icons.Icon({spell.name, type=spellBook.imgType, noicon=true}))
end
table.insert(resultPart, 'Requirements')
if includeDamage then
table.insert(resultPart, 'Spell Dmg')
end
table.insert(resultPart, 'style="width:275px"| Description')
if includeExperience then
table.insert(resultPart, 'XP')
end
table.insert(resultPart, 'style="min-width:90px"| Runes')
if includeItems then
table.insert(resultPart, 'Item Cost')
end
return table.concat(resultPart, '\n! ')
end
 
function p._getSpellRow(spell, includeTypeColumn, includeItems, includeDamage, includeExperience)
local spellBook = p.getSpellBookFromSpell(spell)
local rowPart = {'\n|-\n|data-sort-value="' .. spell.name .. '"| '}
table.insert(rowPart, Icons.Icon({spell.name, type=spellBook.imgType, notext=true, size=50}))
table.insert(rowPart, '|| ' .. Icons.getExpansionIcon(spell.id) .. Icons.Icon({spell.name, type=spellBook.imgType, noicon=true}))
if includeTypeColumn then
table.insert(rowPart, '||data-sort-value="' .. spellBook.id .. '"| ' .. p.getSpellTypeLink(spellBook.id))
end
table.insert(rowPart, '||data-sort-value="' .. spell.level .. '"| ' .. p._getSpellRequirements(spell))
table.insert(rowPart, '||data-sort-value="' .. spell.level .. '"| ' .. p._getSpellRequirements(spell))


if includeTypeColumn then
--11/01/22: Added base damage if requested
table.insert(rowPart, '||data-sort-value="' .. spellBookIdx .. '"| ' .. p.getSpellTypeLink(spell.spellBook))
if includeDamage then
local dmg = p._getSpellStat(spell, 'spellDamage')
table.insert(rowPart, (dmg > 0 and '||style="text-align:right"| ' .. dmg or '||class="table-na"| N/A'))
end
end
--8/20/21: Changed to just getting the spell's description outright
--8/20/21: Changed to just getting the spell's description outright
table.insert(rowPart, '|| ' .. p._getSpellStat(spell, 'description'))
table.insert(rowPart, '|| ' .. p._getSpellStat(spell, 'description'))
--1/4/22: haha just kidding. Now we're also getting delay between attacks for spells with special attacks
--1/4/22: haha just kidding. Now we're also getting delay between attacks for spells with special attacks
if spell.specialAttackID ~= nil then
local spAttID = spell.specialAttackID or spell.specialAttack
local spAtt = Attacks.getAttackByID(spell.specialAttackID)
if spAttID ~= nil then
local spAtt = Attacks.getAttackByID(spAttID)
local interval = spAtt.attackInterval
local interval = spAtt.attackInterval
if interval ~= nil then
local hits = spAtt.attackCount ~= nil and spAtt.attackCount or 1
local hits = spAtt.attackCount ~= nil and spAtt.attackCount or 1
if interval ~= nil and hits > 1 then
            table.insert(rowPart, '<br/>(' .. Shared.round(interval / 1000, 2, 2) .. 's delay between attacks.')
table.insert(rowPart, '<br/>(' .. Shared.round(interval / 1000, 2, 2) .. 's delay between attacks.')
if hits > 2 then
if hits > 2 then
table.insert(rowPart, ' ' .. Shared.round(interval * (hits - 1) / 1000, 2, 2) .. 's total duration.')
table.insert(rowPart, ' ' .. Shared.round(interval * (hits - 1) / 1000, 2, 2) .. 's total duration.')
Line 536: Line 644:
end
end
end
end
if spell.spellBook == 'altMagic' then
if includeExperience then
table.insert(rowPart, '|| ' .. spell.baseExperience)
local xp = spell.baseExperience
table.insert(rowPart, ((xp == nil or xp == 0) and '||class="table-na"| N/A' or '||style="text-align:right"| ' .. xp))
end
end
table.insert(rowPart, '||style="text-align:center"| ' .. p._getSpellRunes(spell))
table.insert(rowPart, '||style="text-align:center"| ' .. p._getSpellRunes(spell))
if includeItems then
table.insert(rowPart, '||style="text-align:center"| ' .. p._getSpellItems(spell))
end
return table.concat(rowPart)
return table.concat(rowPart)
end
end


function p._getSpellBookTable(spellBookID)
function p._getSpellTable(spellList, includeTypeColumn)
    local spells = p.getSpellsBySpellBook(spellBookID)
if type(spellList) == 'table' and not Shared.tableIsEmpty(spellList) then
    if spells ~= nil and not Shared.tableIsEmpty(spells) then
local includeSpellbook, includeItems, includeDamage, includeExperience = false, false, false, false
        local resultPart = {}
if type(includeTypeColumn) == 'boolean' then
        table.insert(resultPart, '{|class="wikitable sortable"\r\n!colspan="2"| Spell')
includeSpellbook = includeTypeColumn
        table.insert(resultPart, '\r\n! Requirements')
end
        table.insert(resultPart, '\r\n!style="width:275px"| Description')
-- Check to see what columns are required
        if spellBookID == 'altMagic' then
for i, spell in ipairs(spellList) do
            table.insert(resultPart, '\r\n! Experience')
local spellBook = p.getSpellBookFromSpell(spell)
        end
if not includeItems and p._getSpellItems(spell) ~= '' then
        table.insert(resultPart, '\r\n! Runes')
includeItems = true
end
if not includeExperience and spellBook.id == 'altMagic' then
includeExperience = true
end
if not includeDamage and (spellBook.id == 'archaic' or spellBook.id == 'standard') then
includeDamage = true
end
end
 
local spellListSorted = Shared.shallowClone(spellList)
table.sort(spellListSorted, function(a, b) return a.level < b.level end)
local resultPart = {p._getSpellHeader(includeSpellbook, includeItems, includeDamage, includeExperience)}
for i, spell in ipairs(spellListSorted) do
table.insert(resultPart, p._getSpellRow(spell, includeSpellbook, includeItems, includeDamage, includeExperience))
end
 
table.insert(resultPart, '\n|}')
return table.concat(resultPart)
end
end


        for i, spell in ipairs(spells) do
function p.getSpellTableFromList(frame)
            table.insert(resultPart, p._getSpellRow(spell, false))
local args = frame.args ~= nil and frame.args or frame
        end
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


        table.insert(resultPart, '\r\n|}')
function p.getSpellBookTable(frame)
        return table.concat(resultPart)
local spellBook = frame.args ~= nil and frame.args[1] or frame[1]
    end
spellBook = p.getSpellBookID(spellBook)
return p._getSpellTable(p.getSpellsBySpellBook(spellBook), false)
end
end


-- Included below for backwards compatibility
-- Included below for backwards compatibility
function p.getStandardSpellsTable(frame)
function p.getStandardSpellsTable(frame)
    return p._getSpellBookTable('standard')
return p._getSpellTable(p.getSpellsBySpellBook('standard'), false)
end
end


function p.getAncientTable(frame)
function p.getAncientTable(frame)
    return p._getSpellBookTable('ancient')
return p._getSpellTable(p.getSpellsBySpellBook('ancient'), false)
end
end


function p.getCurseTable(frame)
function p.getCurseTable(frame)
    return p._getSpellBookTable('curse')
return p._getSpellTable(p.getSpellsBySpellBook('curse'), false)
end
end


function p.getAuroraTable(frame)
function p.getAuroraTable(frame)
    return p._getSpellBookTable('aurora')
return p._getSpellTable(p.getSpellsBySpellBook('aurora'), false)
end
end


function p.getAltSpellsTable(frame)
function p.getAltSpellsTable(frame)
    return p._getSpellBookTable('altMagic')
return p._getSpellTable(p.getSpellsBySpellBook('altMagic'), false)
end
end


return p
return p

Latest revision as of 17:55, 18 June 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')

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 = {
		{
			['type'] = 'SkillLevel',
			['skillID'] = 'melvorD:Magic',
			['level'] = spell.level,
			['abyssalLevel'] = spell.abyssalLevel
		}
	}
	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

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

local function formatRuneList(runes)
	local runeList = {}
	for i, req in ipairs(runes) do
		local rune = Items.getItemByID(req.id)
		if rune ~= nil then
			table.insert(runeList, Icons.Icon({rune.name, type='item', notext=true, qty=req.quantity}))
		end
	end
	return table.concat(runeList, ', ') 
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 or spell.targetModifiers ~= nil then
		local resultPart = {}
		if spell.modifiers ~= nil then
			table.insert(resultPart, Modifiers.getModifiersText(spell.modifiers, false, inline))
		end
		if spell.targetModifiers ~= nil then
			local targetModText = Modifiers.getModifiersText(spell.targetModifiers, false, inline)
			if inline then 
				table.insert(resultPart, targetModText)
			else
				table.insert(resultPart, 'Enemies are inflicted with:<br/>' .. targetModText)
			end
		end
		return table.concat(resultPart, connector)
	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 ' .. Shared.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 spellBook.id == 'standard' or spellBook.id == 'archaic' 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 = Shared.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.level < 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.level < 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)
	local resultPart = {}
	table.insert(resultPart, '{|class="wikitable sortable"\n!colspan="2"| Spell')
	if includeTypeColumn then
		table.insert(resultPart, 'Spellbook')
	end
	table.insert(resultPart, 'Requirements')
	if includeDamage then
		table.insert(resultPart, 'Spell Dmg')
	end
	table.insert(resultPart, 'style="width:275px"| Description')
	if includeExperience then
		table.insert(resultPart, 'XP')
	end
	table.insert(resultPart, 'style="min-width:90px"| Runes')
	if includeItems then
		table.insert(resultPart, 'Item Cost')
	end
	return table.concat(resultPart, '\n! ')
end

function p._getSpellRow(spell, includeTypeColumn, includeItems, includeDamage, includeExperience)
	local spellBook = p.getSpellBookFromSpell(spell)
	local rowPart = {'\n|-\n|data-sort-value="' .. spell.name .. '"| '}
	table.insert(rowPart, Icons.Icon({spell.name, type=spellBook.imgType, notext=true, size=50}))
	table.insert(rowPart, '|| ' .. Icons.getExpansionIcon(spell.id) .. Icons.Icon({spell.name, type=spellBook.imgType, noicon=true}))
	if includeTypeColumn then
		table.insert(rowPart, '||data-sort-value="' .. spellBook.id .. '"| ' .. p.getSpellTypeLink(spellBook.id))
	end
	table.insert(rowPart, '||data-sort-value="' .. spell.level .. '"| ' .. p._getSpellRequirements(spell))

	--11/01/22: Added base damage if requested
	if includeDamage then
		local dmg = p._getSpellStat(spell, 'spellDamage')
		table.insert(rowPart, (dmg > 0 and '||style="text-align:right"| ' .. dmg or '||class="table-na"| N/A'))
	end
	
	--8/20/21: Changed to just getting the spell's description outright
	table.insert(rowPart, '|| ' .. p._getSpellStat(spell, 'description'))
	--1/4/22: haha just kidding. Now we're also getting delay between attacks for spells with special attacks
	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
			table.insert(rowPart, '<br/>(' .. Shared.round(interval / 1000, 2, 2) .. 's delay between attacks.')
			if hits > 2 then
				table.insert(rowPart, ' ' .. Shared.round(interval * (hits - 1) / 1000, 2, 2) .. 's total duration.')
			end
			table.insert(rowPart, ')')
		end
	end
	if includeExperience then
		local xp = spell.baseExperience
		table.insert(rowPart, ((xp == nil or xp == 0) and '||class="table-na"| N/A' or '||style="text-align:right"| ' .. xp))
	end
	table.insert(rowPart, '||style="text-align:center"| ' .. p._getSpellRunes(spell))
	if includeItems then
		table.insert(rowPart, '||style="text-align:center"| ' .. p._getSpellItems(spell))
	end
	return table.concat(rowPart)
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 (spellBook.id == 'archaic' or spellBook.id == 'standard') then
				includeDamage = true
			end
		end

		local spellListSorted = Shared.shallowClone(spellList)
		table.sort(spellListSorted, function(a, b) return a.level < b.level end)
		local resultPart = {p._getSpellHeader(includeSpellbook, includeItems, includeDamage, includeExperience)}
		for i, spell in ipairs(spellListSorted) do
			table.insert(resultPart, p._getSpellRow(spell, includeSpellbook, includeItems, includeDamage, includeExperience))
		end

		table.insert(resultPart, '\n|}')
		return table.concat(resultPart)
	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

-- Included below for backwards compatibility
function p.getStandardSpellsTable(frame)
	return p._getSpellTable(p.getSpellsBySpellBook('standard'), false)
end

function p.getAncientTable(frame)
	return p._getSpellTable(p.getSpellsBySpellBook('ancient'), false)
end

function p.getCurseTable(frame)
	return p._getSpellTable(p.getSpellsBySpellBook('curse'), false)
end

function p.getAuroraTable(frame)
	return p._getSpellTable(p.getSpellsBySpellBook('aurora'), false)
end

function p.getAltSpellsTable(frame)
	return p._getSpellTable(p.getSpellsBySpellBook('altMagic'), false)
end

return p