Module:Magic
From Melvor Idle
Data pulled from Module:GameData/data
local p = {}
local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Attacks = require('Module:Attacks')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
p.spellBooks = {
{ id = 'standard', dataID = 'standardSpells', name = 'Standard Magic', imgType = 'spell' },
{ id = 'ancient', dataID = 'ancientSpells', name = 'Ancient Magick', imgType = 'spell' },
{ id = 'archaic', dataID = 'archaicSpells', name = 'Archaic Magick', imgType = 'spell' },
{ 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', dataBySkill = true }
}
p.spellBookIndex = {}
for i, spellBook in ipairs(p.spellBooks) do
p.spellBookIndex[spellBook.id] = i
end
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 == '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
if spellBook.dataBySkill then
-- Data is part of the Magic skill object
local magicData = GameData.getSkillData('melvorD:Magic')
if magicData ~= nil then
return magicData[spellBook.dataID]
end
else
-- Data is at the root of GameData
return GameData.rawData[spellBook.dataID]
end
end
end
end
function p.getSpell(name, spellType)
local spellBookID = p.getSpellBookID(spellType)
name = Shared.fixPagename(name)
for i, spellBook in ipairs(p.spellBooks) do
if spellBookID == nil or spellBookID == spellBook.id then
local spells = p.getSpellsBySpellBook(spellBook.id)
local spell = GameData.getEntityByName(spells, name)
if spell ~= nil then
return spell
end
end
end
end
function p.getSpellByID(id, spellType)
local spellBookID = p.getSpellBookID(spellType)
if spellType == nil or spellBookID ~= nil then
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
function p.getTypeString(spellType)
local spellBookID = p.getSpellBookID(spellType)
if spellBookID ~= nil then
local spellBook = GameData.getEntityByID(p.spellBooks, spellBookID)
if spellBook ~= nil then
return spellBook.name
end
end
end
function p._getSpellIconType(spell)
local spellBook = GameData.getEntityByID(p.spellBooks, spell.spellBook)
if spellBook == nil then
-- Pick a suitable default
return 'spell'
else
return spellBook.imgType
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 resultPart = { Icons._SkillReq('Magic', spell.level) }
if spell.itemRequiredID ~= nil then
local item = Items.getItemByID(spell.itemRequiredID)
if item ~= nil then
table.insert(resultPart, Icons.Icon({item.name, type='item', notext=true}) .. ' Equipped')
end
end
if spell.requirements ~= nil then
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/>')
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._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 "ERROR: 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
if spell.spellBook == '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)
if itemProduced ~= nil and itemProduced.prayerPoints ~= nil and type(spell.fixedItemCosts) == 'table' then
-- Item produced is a bone
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)
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, Constants.getModifiersText(spell.modifiers, false))
end
if spell.targetModifiers ~= nil then
local targetModText = Constants.getModifiersText(spell.targetModifiers, false)
table.insert(resultPart, 'Enemies are inflicted with:<br/>' .. targetModText)
end
return table.concat(resultPart, '<br/>')
elseif spell.specialAttack ~= nil then
local spAtt = Attacks.getAttackByID(spell.specialAttack)
if spAtt ~= nil then
return spAtt.description
end
elseif spell.spellBook == '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
return p.getTypeString(spell.spellBook)
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 "ERROR: No spell named "..spellName.." found"
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 "ERROR: No spell named "..spellName.." found"
end
local result = ''
--8/20/21: Changed to using the new getSpellDescription function
result = result.."\r\n|-\r\n|'''Description:'''<br/>"..p._getSpellStat(spell, 'description')
return result
end
function p._getSpellCategories(spell)
local result = '[[Category:Spells]]'
result = result..'[[Category:'..p.getTypeString(spell.spellBook)..']]'
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 "ERROR: No spell named "..spellName.." found"
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 = item.type == 'Bar'
isShard = GameData.getEntityByProperty(SkillData.Magic.randomShards, 'itemID', item.id) ~= nil
isGem = GameData.getEntityByProperty('randomGems', 'itemID', itemID) ~= nil
isSuperiorGem = item.type == 'Superior Gem'
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)
if a.spellBook ~= b.spellBook then
return p.spellBookIndex[a.spellBook] < p.spellBookIndex[b.spellBook]
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 == '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._getSpellRow(spell, includeTypeColumn)
local spellBookIdx = p.spellBookIndex[spell.spellBook]
local spellBook = p.spellBooks[spellBookIdx]
local rowPart = {'\r\n|-\r\n|data-sort-value="' .. spell.name .. '"| '}
table.insert(rowPart, Icons.Icon({spell.name, type=spellBook.imgType, notext=true, size=50}))
table.insert(rowPart, '|| ' .. Icons.Icon({spell.name, type=spellBook.imgType, noicon=true}))
table.insert(rowPart, '||data-sort-value="' .. spell.level .. '"| ' .. p._getSpellRequirements(spell))
if includeTypeColumn then
table.insert(rowPart, '||data-sort-value="' .. spellBookIdx .. '"| ' .. p.getSpellTypeLink(spell.spellBook))
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
if spell.specialAttackID ~= nil then
local spAtt = Attacks.getAttackByID(spell.specialAttackID)
local interval = spAtt.attackInterval
if interval ~= nil then
local hits = spAtt.attackCount ~= nil and spAtt.attackCount or 1
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 spell.spellBook == 'altMagic' then
table.insert(rowPart, '|| ' .. spell.baseExperience)
end
table.insert(rowPart, '||style="text-align:center"| ' .. p._getSpellRunes(spell))
return table.concat(rowPart)
end
function p._getSpellBookTable(spellBookID)
local spells = p.getSpellsBySpellBook(spellBookID)
if spells ~= nil and not Shared.tableIsEmpty(spells) then
local resultPart = {}
table.insert(resultPart, '{|class="wikitable sortable"\r\n!colspan="2"| Spell')
table.insert(resultPart, '\r\n! Requirements')
table.insert(resultPart, '\r\n!style="width:275px"| Description')
if spellBookID == 'altMagic' then
table.insert(resultPart, '\r\n! Experience')
end
table.insert(resultPart, '\r\n! Runes')
for i, spell in ipairs(spells) do
table.insert(resultPart, p._getSpellRow(spell, false))
end
table.insert(resultPart, '\r\n|}')
return table.concat(resultPart)
end
end
function p.getSpellBookTable(frame)
local spellBook = frame.args ~= nil and frame.args[1] or frame[1]
spellBook = p.getSpellBookID(spellBook)
return p._getSpellBookTable(spellBook)
end
-- Included below for backwards compatibility
function p.getStandardSpellsTable(frame)
return p._getSpellBookTable('standard')
end
function p.getAncientTable(frame)
return p._getSpellBookTable('ancient')
end
function p.getCurseTable(frame)
return p._getSpellBookTable('curse')
end
function p.getAuroraTable(frame)
return p._getSpellBookTable('aurora')
end
function p.getAltSpellsTable(frame)
return p._getSpellBookTable('altMagic')
end
return p