Anonymous

Module:Skills: Difference between revisions

From Melvor Idle
Update for v1.3
(Adjusted the appearance of the Lesser Relics table)
(Update for v1.3)
Line 18: Line 18:
local Shared = require('Module:Shared')
local Shared = require('Module:Shared')
local Constants = require('Module:Constants')
local Constants = require('Module:Constants')
local Common = require('Module:Common')
local GameData = require('Module:GameData')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local SkillData = GameData.skillData
local Modifiers = require('Module:Modifiers')
local Items = require('Module:Items')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
Line 44: Line 46:
["Runecrafting"] = 'recipes',
["Runecrafting"] = 'recipes',
["Herblore"] = 'recipes',
["Herblore"] = 'recipes',
["Astrology"] = 'recipes'
["Astrology"] = 'recipes',
["Harvesting"] = 'veinData'
}
}
return recipeIDs[localSkillID]
return recipeIDs[localSkillID]
Line 50: Line 53:


-- Given a skill ID & recipe, returns the skill level requirement for
-- Given a skill ID & recipe, returns the skill level requirement for
-- that recipe. If the level could not be determined, then the return
-- that recipe and a boolean value indicating whether the level if abyssal or not.
-- value is nil
-- If the level could not be determined, then the return value is nil, nil
function p.getRecipeLevel(skillID, recipe)
function p.getRecipeLevelRealm(skillID, recipe)
local level, isAbyssal = nil, nil
-- Convert skillID to local ID if not already
-- Convert skillID to local ID if not already
local ns, localSkillID = GameData.getLocalID(skillID)
local ns, localSkillID = GameData.getLocalID(skillID)
local realmID = p.getRecipeRealm(recipe)
if localSkillID == 'Agility' then
if localSkillID == 'Agility' then
local course = GameData.getEntityByProperty(SkillData.Agility.courses, 'realm', realmID)
isAbyssal = (realmID == 'melvorItA:Abyssal')
-- For Agility, level is derived from obstacle category
-- For Agility, level is derived from obstacle category
if recipe.category ~= nil then
if recipe.category ~= nil then
-- Obstacle
-- Obstacle
return SkillData.Agility.obstacleUnlockLevels[recipe.category+1]
level = course.obstacleSlots[recipe.category + 1].level
else
elseif recipe.slot ~= nil then
-- Pillar
-- Pillar
local nsR, localRecipeID = GameData.getLocalID(recipe.id)
level = course.pillarSlots[recipe.slot + 1].level
if localRecipeID ~= nil then
if string.find(localRecipeID, '^Pillar') ~= nil then
return 99
elseif string.find(localRecipeID, '^ElitePillar') ~= nil then
return 120
end
end
end
end
elseif recipe.abyssalLevel ~= nil then
level, isAbyssal = recipe.abyssalLevel, true
else
else
-- For all other skills, the recipe should have a level property
-- For all other skills, the recipe should have a level property
return recipe.level
level, isAbyssal = recipe.level, false
end
return level, isAbyssal
end
 
function p.getRecipeLevel(skillID, recipe)
local level, isAbyssal = p.getRecipeLevelRealm(skillID, recipe)
return level
end
 
function p.getRecipeRequirementText(skillName, recipe)
local reqText = {}
if recipe.abyssalLevel ~= nil and recipe.abyssalLevel > 0 then
table.insert(reqText, Icons._AbyssalSkillReq(skillName, recipe.abyssalLevel, false))
elseif recipe.level ~= nil and recipe.level > 0 then
table.insert(reqText, Icons._SkillReq(skillName, recipe.level, false))
end
if recipe.totalMasteryRequired ~= nil then
table.insert(reqText, Shared.formatnum(recipe.totalMasteryRequired) .. ' ' .. Icons.Icon({skillName, type='skill', notext=true}) .. ' ' .. Icons.Icon({'Mastery'}))
end
local reqsData = {}
if type(recipe.requirements) == 'table' then
reqsData = Shared.shallowClone(recipe.requirements)
end
if recipe.shopItemPurchased ~= nil then
-- Mining requirements are stored differently than other skills like
-- Woodcutting, standardize here
table.insert(reqsData, {
["type"] = 'ShopPurchase',
["purchaseID"] = recipe.shopItemPurchased,
["count"] = 1
})
end
if not Shared.tableIsEmpty(reqsData) then
local reqs = Common.getRequirementString(reqsData)
if reqs ~= nil then
table.insert(reqText, reqs)
end
end
return table.concat(reqText, '<br/>')
end
 
function p.standardRecipeSort(skillID, recipeA, recipeB)
local levelA, isAbyssalA = p.getRecipeLevelRealm(skillID, recipeA)
local levelB, isAbyssalB = p.getRecipeLevelRealm(skillID, recipeB)
local isAbyssalNumA, isAbyssalNumB = (isAbyssalA and 1) or 0, (isAbyssalB and 1) or 0
 
return (isAbyssalA == isAbyssalB and levelA < levelB) or isAbyssalNumA < isAbyssalNumB
end
 
 
function p.getRecipeRealm(recipe)
return recipe.realm or 'melvorD:Melvor'
end
 
function p.getRealmFromName(realmName)
local realm = nil
if realmName == nil or realmName == '' then
-- Default realm
realm = GameData.getEntityByID('realms', 'melvorD:Melvor')
else
realm = GameData.getEntityByName('realms', realmName)
end
end
return realm
end
function p.getMasteryActionCount(skillID, realmID, levelLimit)
local actCount = 0
local skillNS, skillLocalID = Shared.getLocalID(skillID)
local skillData = SkillData[skillLocalID]
local recipeKey = p.getSkillRecipeKey(skillLocalID)
if recipeKey ~= nil then
local recipeData = skillData[recipeKey]
for i, recipe in ipairs(recipeData) do
if (
p.getRecipeRealm(recipe) == realmID
and (recipe.noMastery == nil or not recipe.noMastery)
and (levelLimit == nil or p.getRecipeLevel(skillLocalID, recipe) <= levelLimit)
) then
actCount = actCount + 1
end
end
end
return actCount
end
end


Line 203: Line 287:
end
end


-- For a given constellation cons and modifier value modValue, generates and returns
-- Combines Astrology constellation modifiers into an object similar to other entities,
-- a table of modifiers, much like any other item/object elsewhere in the game.
-- and multiplies the values up to their maximum possible amount
-- includeStandard: true|false, determines whether standard modifiers are included
function p._getConstellationModifiers(cons)
-- includeUnique: true|false, determines whether unique modifiers are included
local result = {}
-- isDistinct: true|false, if true, the returned list of modifiers is de-duplicated
local modKeys = { 'standardModifiers', 'uniqueModifiers', 'abyssalModifiers' }
-- asKeyValue: true|false, if true, returns key/value pairs like usual modifier objects
 
function p._buildAstrologyModifierArray(cons, modValue, includeStandard, includeUnique, isDistinct, asKeyValue)
for _, keyID in ipairs(modKeys) do
-- Temporary function to determine if the table already contains a given modifier
result[keyID] = {}
local containsMod = function(modList, modNew)
local mods = cons[keyID]
for i, modItem in ipairs(modList) do
if mods ~= nil then
-- Check mod names & value data types both equal
for _, mod in ipairs(mods) do
if modItem[1] == modNew[1] and type(modItem[2]) == type(modNew[2]) then
local multValue = mod.maxCount
if type(modItem[2]) == 'table' then
local subKeys = { 'modifiers', 'enemyModifiers' }
if Shared.tablesEqual(modItem[2], modNew[2]) then
for _, subKey in ipairs(subKeys) do
return true
local modAdj = Shared.clone(mod[subKey])
if type(modAdj) == 'table' then
for modName, modValueDef in pairs(modAdj) do
if type(modValueDef) == 'table' then
if modValueDef[1] ~= nil then
-- Table of multiple values
for i, subValue in ipairs(modValueDef) do
if type(subValue) == 'table' and subValue.value ~= nil then
subValue.value = subValue.value * multValue
elseif type(subValue) == 'number' then
modValueDef[i] = subValue * multValue
end
end
elseif modValueDef.value ~= nil then
-- Table but with a single value
modValueDef.value = modValueDef.value * multValue
end
elseif type(modValueDef) == 'number' then
-- Single value
modAdj[modName] = modValueDef * multValue
end
end
end
elseif modItem[2] == modNew[2] then
return true
end
end
table.insert(result[keyID], modAdj)
end
end
end
end
return false
end
end
local addToArray = function(modArray, modNew)
if not isDistinct or (isDistinct and not containsMod(modArray, modNew)) then
table.insert(modArray, modNew)
end
end
local modTypes = {}
if includeStandard then
table.insert(modTypes, 'standardModifiers')
end
if includeUnique then
table.insert(modTypes, 'uniqueModifiers')
end
local masteryReq = {
['standardModifiers'] = { 1, 40, 80 },
['uniqueModifiers'] = { 20, 60, 99 }
}
local modArray = {}
local isSkillMod = {}
--Adding a Group Number to hold together different bonuses from the same modifier [Falterfire 22/10/27]
local groupNum = 0
for _, modType in ipairs(modTypes) do
for i, modTypeData in ipairs(cons[modType]) do
groupNum = masteryReq[modType][i]
local modVal = nil
if modValue ~= nil then
modVal = modValue
else
modVal = modTypeData.incrementValue * modTypeData.maxCount
end
for j, modifier in ipairs(modTypeData.modifiers) do
local modEntry = (modifier.skill ~= nil and { skillID = modifier.skill, value = modVal }) or modVal
addToArray(modArray, {modifier.key, modEntry, group = groupNum})
end
end
end
if asKeyValue then
local modArrayKV = {}
for i, modDefn in ipairs(modArray) do
local modName, modVal = modDefn[1], modDefn[2]
local isSkill = type(modVal) == 'table' and modVal.skillID ~= nil
if modArrayKV[modName] == nil then
modArrayKV[modName] = (isSkill and { modVal } or modVal)
elseif isSkill then
table.insert(modArrayKV[modName], modVal)
else
modArrayKV[modName] = modArrayKV[modName] + modVal
end
end
return modArrayKV
else
return modArray
end
end
return result
end
end


Line 295: Line 342:
local _, localSkillID = GameData.getLocalID(skillID)
local _, localSkillID = GameData.getLocalID(skillID)
-- Clone so that we can sort by level
-- Clone so that we can sort by level
local unlockTable = Shared.clone(SkillData[localSkillID].masteryLevelUnlocks)
local unlockTable = Shared.shallowClone(SkillData[localSkillID].masteryLevelUnlocks)
if unlockTable == nil then
if unlockTable == nil then
return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
Line 311: Line 358:


function p.getMasteryCheckpointTable(frame)
function p.getMasteryCheckpointTable(frame)
local skillName = frame.args ~= nil and frame.args[1] or frame
local skillName = frame.args ~= nil and frame.args[1]
local realmName = frame.args ~= nil and frame.args[2]
local skillID = Constants.getSkillID(skillName)
local skillID = Constants.getSkillID(skillName)
local realm = nil
if realmName ~= nil then
realm = GameData.getEntityByName('realms', realmName)
else
realm = GameData.getEntityByID('realms', 'melvorD:Melvor')
end
if skillID == nil then
if skillID == nil then
return Shared.printError('Failed to find a skill ID for ' .. skillName)
return Shared.printError('Failed to find a skill ID for ' .. skillName)
elseif realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
end


local _, localSkillID = GameData.getLocalID(skillID)
local _, localSkillID = GameData.getLocalID(skillID)
local checkpoints = SkillData[localSkillID].masteryCheckpoints
local checkpoints = SkillData[localSkillID].masteryPoolBonuses
if checkpoints == nil then
if checkpoints == nil then
return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
return Shared.printError('Failed to find Mastery checkpoint data for ' .. skillName)
end
end


local totalPoolXP = SkillData[localSkillID].baseMasteryPoolCap
local totalPoolXP = p.getMasteryActionCount(localSkillID, realm.id) * 500000
local checkpointPct = GameData.rawData.masteryCheckpoints
local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
for i, checkpointDesc in ipairs(checkpoints) do
for i, checkpointData in ipairs(checkpoints) do
result = result..'\r\n|-'
if checkpointData.realm == realm.id then
result = result..'\r\n|'..checkpointPct[i]..'%||'
local chkDesc = Modifiers.getModifiersText(checkpointData.modifiers, false, false)
result = result..Shared.formatnum(math.floor(totalPoolXP * checkpointPct[i] / 100))..' xp||'..checkpointDesc
local chkPercent = checkpointData.percent
result = result..'\r\n|-'
result = result..'\r\n|'..chkPercent..'%||'
result = result..Shared.formatnum(math.floor(totalPoolXP * chkPercent / 100))..' xp||'..chkDesc
end
end
end
result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
Line 356: Line 416:
if CCI == nil then
if CCI == nil then
return Shared.printError('Failed to find item with ID ' .. CCI_ID)
return Shared.printError('Failed to find item with ID ' .. CCI_ID)
end
local tokens = Items.getItems(function(item) return item.itemType == 'MasteryToken' end)
local tokenItems = {}
for _, item in ipairs(tokens) do
if item.realm == 'melvorD:Melvor' and item.skill ~= nil then
local skillNS, skillLocalID = Shared.getLocalID(item.skill)
tokenItems[skillLocalID] = item
end
end
end


Line 361: Line 430:
-- mastery actions for each
-- mastery actions for each
for skillLocalID, skill in pairs(SkillData) do
for skillLocalID, skill in pairs(SkillData) do
if skill.masteryTokenID ~= nil then
if skill.masteryPoolBonuses ~= nil then
local actCount = { ["skill"] = skill }
local actCount = { ["skill"] = skill, ["token"] = tokenItems[skillLocalID] }
for i, levelDef in ipairs(skillLevels) do
for i, levelDef in ipairs(skillLevels) do
actCount[levelDef.id] = 0
actCount[levelDef.id] = p.getMasteryActionCount(skillLocalID, 'melvorD:Melvor', levelDef.level)
end
 
local recipeKey = p.getSkillRecipeKey(skillLocalID)
if recipeKey ~= nil then
local recipeData = skill[recipeKey]
for i, recipe in ipairs(recipeData) do
if recipe.noMastery == nil or not recipe.noMastery then
local skillLevel = p.getRecipeLevel(skillLocalID, recipe)
if skillLevel ~= nil then
for j, levelDef in ipairs(skillLevels) do
if skillLevel <= levelDef.level then
actCount[levelDef.id] = actCount[levelDef.id] + 1
end
end
end
end
end
end
end
table.insert(masteryActionCount, actCount)
table.insert(masteryActionCount, actCount)
Line 412: Line 464:


for i, rowData in ipairs(masteryActionCount) do
for i, rowData in ipairs(masteryActionCount) do
local token = Items.getItemByID(rowData.skill.masteryTokenID)
local token = rowData.token
table.insert(resultPart, '\n|-')
table.insert(resultPart, '\n|-')
table.insert(resultPart, '\n|style="text-align:center"|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
local tokenImg = (token == nil and '?') or Icons.Icon({token.name, type='item', size=50, notext=true})
table.insert(resultPart, '\n|style="text-align:center"|' .. tokenImg)
table.insert(resultPart, '\n|' .. Icons.Icon({rowData.skill.name, type='skill'}))
table.insert(resultPart, '\n|' .. Icons.Icon({rowData.skill.name, type='skill'}))


Line 422: Line 475:
if actCount > 0 then
if actCount > 0 then
denom = math.floor(baseTokenChance / actCount)
denom = math.floor(baseTokenChance / actCount)
denomCCI = Shared.round(baseTokenChance / (actCount * (1 + CCI.modifiers.increasedOffItemChance / 100)), 0, 0)
denomCCI = Shared.round(baseTokenChance / (actCount * (1 + CCI.modifiers.offItemChance / 100)), 0, 0)
end
end
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Shared.formatnum(denom))
table.insert(resultPart, '\n|style="text-align:right" data-sort-value="' .. denom .. '"|1/' .. Shared.formatnum(denom))
Line 456: Line 509:


function p.getFiremakingTable(frame)
function p.getFiremakingTable(frame)
local args = frame.args ~= nil and frame.args or frame
local realmName = args.realm
local realm = p.getRealmFromName(realmName)
if realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
local skillID = 'Firemaking'
local resultPart = {}
local resultPart = {}
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
table.insert(resultPart, '\r\n|-class="headerRow-0"')
table.insert(resultPart, '\r\n|-class="headerRow-0"')
table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|'..Icons.Icon({'Firemaking', type='skill', notext=true})..' Level')
table.insert(resultPart, '\r\n!colspan="2" rowspan="2"|Logs!!rowspan="2"|Requirements')
table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
table.insert(resultPart, '!!rowspan="2"|Burn Time!!colspan="2"|Without Bonfire!!colspan="2"|With Bonfire!!rowspan="2"|Bonfire Bonus!!rowspan="2"|Bonfire Time')
table.insert(resultPart, '\r\n|-class="headerRow-1"')
table.insert(resultPart, '\r\n|-class="headerRow-1"')
table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')
table.insert(resultPart, '\r\n!XP!!XP/s!!XP!!XP/s')


for i, logData in ipairs(SkillData.Firemaking.logs) do
local logsData = GameData.getEntities(SkillData.Firemaking.logs,
function(obj)
return p.getRecipeRealm(obj) == realm.id
end
)
table.sort(logsData, function(a, b) return p.standardRecipeSort(skillID, a, b) end)
for i, logData in ipairs(logsData) do
local logs = Items.getItemByID(logData.logID)
local logs = Items.getItemByID(logData.logID)
local name = logs.name
local name = logs.name
local level = p.getRecipeLevel(skillID, logData)
local baseXP = logData.baseAbyssalExperience or logData.baseExperience
local reqText = p.getRecipeRequirementText(SkillData.Firemaking.name, logData)
local bonfireBonus = logData.bonfireAXPBonus or logData.bonfireXPBonus
local burnTime = logData.baseInterval / 1000
local burnTime = logData.baseInterval / 1000
local bonfireTime = logData.baseBonfireInterval / 1000
local bonfireTime = logData.baseBonfireInterval / 1000
local XPS = logData.baseExperience / burnTime
local XPS = baseXP / burnTime
local XP_BF = logData.baseExperience * (1 + logData.bonfireXPBonus / 100)
local XP_BF = baseXP * (1 + bonfireBonus / 100)
local XPS_BF = Shared.round(XP_BF / burnTime, 2, 2)
local XPS_BF = Shared.round(XP_BF / burnTime, 2, 2)
XP_BF = Shared.round(XP_BF, 2, 0)
XP_BF = Shared.round(XP_BF, 2, 0)
Line 477: Line 548:
table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
table.insert(resultPart, '\r\n|data-sort-value="'..name..'"|'..Icons.Icon({name, type='item', size='50', notext=true}))
table.insert(resultPart, '||'..Icons.getExpansionIcon(logs.id)..Icons.Icon({name, type='item', noicon=true}))
table.insert(resultPart, '||'..Icons.getExpansionIcon(logs.id)..Icons.Icon({name, type='item', noicon=true}))
table.insert(resultPart, '||style ="text-align: right;"|'..logData.level)
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. level .. '"|'..reqText)
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..burnTime..'"|'..Shared.timeString(burnTime, true))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. logData.baseExperience .. '"| ' .. Shared.formatnum(logData.baseExperience))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. baseXP .. '"| ' .. Shared.formatnum(baseXP))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.formatnum(Shared.round(XPS, 2, 2)))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS..'"|'..Shared.formatnum(Shared.round(XPS, 2, 2)))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. XP_BF .. '"| ' .. Shared.formatnum(XP_BF))
if bonfireBonus == 0 then
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.formatnum(XPS_BF, 2, 2))
table.insert(resultPart, '||colspan="4" class="table-na"| N/A ')
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..logData.bonfireXPBonus..'"|'..logData.bonfireXPBonus..'%')
else
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireTime..'"|'..Shared.timeString(bonfireTime, true))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="' .. XP_BF .. '"| ' .. Shared.formatnum(XP_BF))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..XPS_BF..'"|'..Shared.formatnum(XPS_BF, 2, 2))
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireBonus..'"|'..bonfireBonus..'%')
table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireTime..'"|'..Shared.timeString(bonfireTime, true))
end
end
end