Module:Magic: Difference between revisions
From Melvor Idle
Falterfire (talk | contribs) (Fixed some mistakes with new getting spells) |
(Add damage column for abyssal spell tables, add initial combat effects handling) |
||
(88 intermediate revisions by 5 users not shown) | |||
Line 1: | Line 1: | ||
local p = {} | local p = {} | ||
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 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 | end | ||
function p. | 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 | end | ||
function p. | 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 | 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