|
|
Line 1: |
Line 1: |
| --This module should avoid including skill specific functions which generate
| |
| --output for wiki pages, especially those which require() other modules. For
| |
| --these functions, consider using the appropriate module from the below list.
| |
|
| |
|
| --Some skills have their own modules:
| |
| --Module:Magic for Magic
| |
| --Module:Prayer for Prayer
| |
| --Module:Skills/Agility for Agility
| |
| --Module:Skills/Summoning for Summoning
| |
| --Module:Skills/Gathering for Mining, Fishing, Woodcutting
| |
| --Module:Skills/Artisan for Smithing, Cooking, Herblore, etc.
| |
|
| |
| --Also be aware of:
| |
| --Module:Navboxes for navigation boxes appearing near the bottom of pages
| |
|
| |
| local p = {}
| |
|
| |
| local Shared = require('Module:Shared')
| |
| local Constants = require('Module:Constants')
| |
| local GameData = require('Module:GameData')
| |
| local SkillData = GameData.skillData
| |
| local Items = require('Module:Items')
| |
| local Icons = require('Module:Icons')
| |
|
| |
| -- Given a skill ID, returns the key for that skill's recipe data.
| |
| -- If the skill has no recipes (e.g. is a combat skill) then the
| |
| -- return value is nil
| |
| function p.getSkillRecipeKey(skillID)
| |
| -- Convert skillID to local ID if not already
| |
| local ns, localSkillID = GameData.getLocalID(skillID)
| |
| local recipeIDs = {
| |
| ["Woodcutting"] = 'trees',
| |
| ["Fishing"] = 'fish',
| |
| ["Firemaking"] = 'logs',
| |
| ["Mining"] = 'rockData',
| |
| ["Thieving"] = 'npcs',
| |
| ["Agility"] = 'obstacles',
| |
| ["Cooking"] = 'recipes',
| |
| ["Smithing"] = 'recipes',
| |
| ["Farming"] = 'recipes',
| |
| ["Summoning"] = 'recipes',
| |
| ["Fletching"] = 'recipes',
| |
| ["Crafting"] = 'recipes',
| |
| ["Runecrafting"] = 'recipes',
| |
| ["Herblore"] = 'recipes',
| |
| ["Astrology"] = 'recipes'
| |
| }
| |
| return recipeIDs[localSkillID]
| |
| end
| |
|
| |
| -- Given a skill ID & recipe, returns the skill level requirement for
| |
| -- that recipe. If the level could not be determined, then the return
| |
| -- value is nil
| |
| function p.getRecipeLevel(skillID, recipe)
| |
| -- Convert skillID to local ID if not already
| |
| local ns, localSkillID = GameData.getLocalID(skillID)
| |
| if localSkillID == 'Agility' then
| |
| if recipe.category ~= nil then
| |
| -- Obstacle
| |
| return SkillData.Agility.obstacleUnlockLevels[recipe.category+1]
| |
| else
| |
| -- Pillar
| |
| local nsR, localRecipeID = GameData.getLocalID(recipe.id)
| |
| 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
| |
| else
| |
| -- For all other skills, the recipe should have a level property
| |
| return recipe.level
| |
| end
| |
| end
| |
|
| |
| -- Thieving
| |
| function p.getThievingNPCByID(npcID)
| |
| return GameData.getEntityByID(SkillData.Thieving.npcs, npcID)
| |
| end
| |
|
| |
| function p.getThievingNPC(npcName)
| |
| return GameData.getEntityByName(SkillData.Thieving.npcs, npcName)
| |
| end
| |
|
| |
| function p.getThievingNPCArea(npc)
| |
| for i, area in ipairs(SkillData.Thieving.areas) do
| |
| for j, npcID in ipairs(area.npcIDs) do
| |
| if npcID == npc.id then
| |
| return area
| |
| end
| |
| end
| |
| end
| |
| end
| |
|
| |
| function p._getThievingNPCStat(npc, statName)
| |
| local result = nil
| |
|
| |
| if statName == 'level' then
| |
| result = Icons._SkillReq('Thieving', npc.level)
| |
| elseif statName == 'maxHit' then
| |
| result = npc.maxHit * 10
| |
| elseif statName == 'area' then
| |
| local area = p.getThievingNPCArea(npc)
| |
| result = area.name
| |
| else
| |
| result = npc[statName]
| |
| end
| |
|
| |
| if result == nil then
| |
| result = ''
| |
| end
| |
|
| |
| return result
| |
| end
| |
|
| |
| function p.getThievingNPCStat(frame)
| |
| local npcName = frame.args ~= nil and frame.args[1] or frame[1]
| |
| local statName = frame.args ~= nil and frame.args[2] or frame[2]
| |
| local npc = p.getThievingNPC(npcName)
| |
| if npc == nil then
| |
| return Shared.printError('Invalid Thieving NPC ' .. npcName)
| |
| end
| |
|
| |
| return p._getThievingNPCStat(npc, statName)
| |
| end
| |
|
| |
| function p.getThievingSourcesForItem(itemID)
| |
| local resultArray = {}
| |
| local areaNPCs = {}
| |
|
| |
| --First check area unique drops
| |
| --If an area drops the item, add all the NPC ids to the list so we can add them later
| |
| for i, area in pairs(SkillData.Thieving.areas) do
| |
| for j, drop in pairs(area.uniqueDrops) do
| |
| if drop.id == itemID then
| |
| for k, npcID in ipairs(area.npcIDs) do
| |
| areaNPCs[npcID] = { qty = drop.quantity, area = area }
| |
| end
| |
| break
| |
| end
| |
| end
| |
| end
| |
|
| |
| --Now go through and get drop chances on each NPC if needed
| |
| for i, npc in pairs(SkillData.Thieving.npcs) do
| |
| local totalWt = 0
| |
| local dropWt = 0
| |
| local dropQty = { min = 0, max = 0 }
| |
| for j, drop in ipairs(npc.lootTable) do
| |
| totalWt = totalWt + drop.weight
| |
| if drop.itemID == itemID then
| |
| dropWt = drop.weight
| |
| dropQty = { min = drop.minQuantity, max = drop.maxQuantity }
| |
| end
| |
| end
| |
| if dropWt > 0 then
| |
| table.insert(resultArray, {npc = npc.name, minQty = dropQty.min, maxQty = dropQty.max, wt = dropWt * SkillData.Thieving.itemChance, totalWt = totalWt * 100, level = npc.level, npcID = npc.id, type = 'npc'})
| |
| end
| |
|
| |
| --Chance of -1 on unique drops is to indicate variable chance
| |
| if npc.uniqueDrop ~= nil and npc.uniqueDrop.id == itemID then
| |
| table.insert(resultArray, {npc = npc.name, minQty = npc.uniqueDrop.quantity, maxQty = npc.uniqueDrop.quantity, wt = -1, totalWt = -1, level = npc.level, npcID = npc.id, type = 'npcUnique'})
| |
| end
| |
|
| |
| local areaNPC = areaNPCs[npc.id]
| |
| if areaNPC ~= nil then
| |
| table.insert(resultArray, {npc = npc.name, minQty = areaNPC.qty, maxQty = areaNPC.qty, wt = SkillData.Thieving.baseAreaUniqueChance, totalWt = 100, level = npc.level, npcID = npc.id, area = areaNPC.area, type = 'areaUnique'})
| |
| end
| |
| end
| |
|
| |
| for i, drop in ipairs(SkillData.Thieving.generalRareItems) do
| |
| if drop.itemID == itemID then
| |
| if drop.npcs == nil then
| |
| table.insert(resultArray, {npc = 'all', minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = 1, npcID = itemID, type = 'generalRare'})
| |
| else
| |
| for j, npcID in ipairs(drop.npcs) do
| |
| local npc = p.getThievingNPCByID(npcID)
| |
| if npc ~= nil then
| |
| table.insert(resultArray, {npc = npc.name, minQty = 1, maxQty = 1, wt = 1, totalWt = Shared.round2(1/(drop.chance/100), 0), level = npc.level, npcID = npc.id, type = 'generalRare'})
| |
| end
| |
| end
| |
| end
| |
| end
| |
| end
| |
|
| |
| return resultArray
| |
| end
| |
|
| |
| -- Astrology
| |
| function p.getConstellationByID(constID)
| |
| return GameData.getEntityByID(SkillData.Astrology.recipes, constID)
| |
| end
| |
|
| |
| function p.getConstellation(constName)
| |
| return GameData.getEntityByName(SkillData.Astrology.recipes, constName)
| |
| end
| |
|
| |
| function p.getConstellations(checkFunc)
| |
| return GameData.getEntities(SkillData.Astrology.recipes, checkFunc)
| |
| end
| |
|
| |
| -- For a given constellation cons and modifier value modValue, generates and returns
| |
| -- a table of modifiers, much like any other item/object elsewhere in the game.
| |
| -- includeStandard: true|false, determines whether standard modifiers are included
| |
| -- includeUnique: true|false, determines whether unique modifiers are included
| |
| -- isDistinct: true|false, if true, the returned list of modifiers is de-duplicated
| |
| -- asKeyValue: true|false, if true, returns key/value pairs like usual modifier objects
| |
| function p._buildAstrologyModifierArray(cons, modValue, includeStandard, includeUnique, isDistinct, asKeyValue)
| |
| -- Temporary function to determine if the table already contains a given modifier
| |
| local containsMod = function(modList, modNew)
| |
| for i, modItem in ipairs(modList) do
| |
| -- Check mod names & value data types both equal
| |
| if modItem[1] == modNew[1] and type(modItem[2]) == type(modNew[2]) then
| |
| if type(modItem[2]) == 'table' then
| |
| if Shared.tablesEqual(modItem[2], modNew[2]) then
| |
| return true
| |
| end
| |
| elseif modItem[2] == modNew[2] then
| |
| return true
| |
| end
| |
| end
| |
| end
| |
| return false
| |
| 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
| |
|
| |
| -- Mastery
| |
| function p.getMasteryUnlockTable(frame)
| |
| local skillName = frame.args ~= nil and frame.args[1] or frame
| |
| local skillID = Constants.getSkillID(skillName)
| |
| if skillID == nil then
| |
| return Shared.printError('Failed to find a skill ID for ' .. skillName)
| |
| end
| |
|
| |
| local _, localSkillID = GameData.getLocalID(skillID)
| |
| -- Clone so that we can sort by level
| |
| local unlockTable = Shared.clone(SkillData[localSkillID].masteryLevelUnlocks)
| |
| if unlockTable == nil then
| |
| return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
| |
| end
| |
| table.sort(unlockTable, function(a, b) return (a.level == b.level and a.descriptionID < b.descriptionID) or a.level < b.level end)
| |
|
| |
| local result = '{|class="wikitable"\r\n!Level!!Unlock'
| |
| for i, unlock in ipairs(unlockTable) do
| |
| result = result..'\r\n|-'
| |
| result = result..'\r\n|'..unlock.level..'||'..unlock.description
| |
| end
| |
| result = result..'\r\n|}'
| |
| return result
| |
| end
| |
|
| |
| function p.getMasteryCheckpointTable(frame)
| |
| local skillName = frame.args ~= nil and frame.args[1] or frame
| |
| local skillID = Constants.getSkillID(skillName)
| |
| if skillID == nil then
| |
| return Shared.printError('Failed to find a skill ID for ' .. skillName)
| |
| end
| |
|
| |
| local _, localSkillID = GameData.getLocalID(skillID)
| |
| local checkpoints = SkillData[localSkillID].masteryCheckpoints
| |
| if checkpoints == nil then
| |
| return Shared.printError('Failed to find Mastery Unlock data for ' .. skillName)
| |
| end
| |
|
| |
| local totalPoolXP = SkillData[localSkillID].baseMasteryPoolCap
| |
| local checkpointPct = GameData.rawData.masteryCheckpoints
| |
| local result = '{|class="wikitable"\r\n!Pool %!!style="width:100px"|Pool XP!!Bonus'
| |
| for i, checkpointDesc in ipairs(checkpoints) do
| |
| result = result..'\r\n|-'
| |
| result = result..'\r\n|'..checkpointPct[i]..'%||'
| |
| result = result..Shared.formatnum(math.floor(totalPoolXP * checkpointPct[i] / 100))..' xp||'..checkpointDesc
| |
| end
| |
| result = result..'\r\n|-\r\n!colspan="2"|Total Mastery Pool XP'
| |
| result = result..'\r\n|'..Shared.formatnum(totalPoolXP)
| |
| result = result..'\r\n|}'
| |
| return result
| |
| end
| |
|
| |
| function p.getMasteryTokenTable()
| |
| -- Defines which skill levels should be included within the output
| |
| local skillLevels = {
| |
| {
| |
| ["id"] = 'Base',
| |
| ["level"] = 99,
| |
| ["description"] = '[[Full Version|Base Game]] (Level 99)'
| |
| }, {
| |
| ["id"] = 'TotH',
| |
| ["level"] = 120,
| |
| ["description"] = Icons.TotH() .. ' [[Throne of the Herald Expansion|Throne of the Herald]] (Level 120)'
| |
| }
| |
| }
| |
| local baseTokenChance = 18500
| |
| local masteryActionCount = {}
| |
| local CCI_ID = 'melvorD:Clue_Chasers_Insignia'
| |
| local CCI = Items.getItemByID(CCI_ID)
| |
| if CCI == nil then
| |
| return Shared.printError('Failed to find item with ID ' .. CCI_ID)
| |
| end
| |
|
| |
| -- Iterate over each skill with mastery, determining the number of
| |
| -- mastery actions for each
| |
| for skillLocalID, skill in pairs(SkillData) do
| |
| if skill.masteryTokenID ~= nil then
| |
| local actCount = { ["skill"] = skill }
| |
| for i, levelDef in ipairs(skillLevels) do
| |
| actCount[levelDef.id] = 0
| |
| 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
| |
| table.insert(masteryActionCount, actCount)
| |
| end
| |
| end
| |
|
| |
| local firstID = skillLevels[1].id
| |
| table.sort(masteryActionCount,
| |
| function(a, b)
| |
| if a[firstID] == b[firstID] then
| |
| return a.skill.name < b.skill.name
| |
| else
| |
| return a[firstID] > b[firstID]
| |
| end
| |
| end)
| |
|
| |
| -- Generate output table
| |
| local resultPart = {}
| |
| local CCIIcon = Icons.Icon({CCI.name, type='item', notext=true})
| |
| local columnPairs = Shared.tableCount(skillLevels)
| |
|
| |
| -- Generate header
| |
| table.insert(resultPart, '{| class="wikitable sortable"')
| |
| table.insert(resultPart, '\n!rowspan="3"|Token!!rowspan="3"|Skill!!colspan="' .. columnPairs * 2 .. '"|Approximate Mastery Token Chance')
| |
| table.insert(resultPart, '\n|-')
| |
| for i, levelDef in ipairs(skillLevels) do
| |
| table.insert(resultPart, '\n!colspan="2"| ' .. levelDef.description)
| |
| end
| |
| table.insert(resultPart, '\n|-' .. string.rep('\n!Without ' .. CCIIcon .. '\n!With ' .. CCIIcon, columnPairs))
| |
|
| |
| for i, rowData in ipairs(masteryActionCount) do
| |
| local token = Items.getItemByID(rowData.skill.masteryTokenID)
| |
| table.insert(resultPart, '\n|-')
| |
| table.insert(resultPart, '\n|style="text-align:center"|' .. Icons.Icon({token.name, type='item', size=50, notext=true}))
| |
| table.insert(resultPart, '\n|' .. Icons.Icon({rowData.skill.name, type='skill'}))
| |
|
| |
| for j, levelDef in ipairs(skillLevels) do
| |
| local actCount = rowData[levelDef.id]
| |
| local denom, denomCCI = 0, 0
| |
| if actCount > 0 then
| |
| denom = math.floor(baseTokenChance / actCount)
| |
| denomCCI = Shared.round(baseTokenChance / (actCount * (1 + CCI.modifiers.increasedOffItemChance / 100)), 0, 0)
| |
| 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="' .. denomCCI .. '"|1/' .. Shared.formatnum(denomCCI))
| |
| end
| |
| end
| |
| table.insert(resultPart, '\n|}')
| |
|
| |
| return table.concat(resultPart)
| |
| end
| |
|
| |
| -- Skill unlock costs for Adventure game mode
| |
| function p.getSkillUnlockCostTable()
| |
| local advMode = GameData.getEntityByID('gamemodes', 'melvorF:Adventure')
| |
| if advMode ~= nil then
| |
| local unlockCount = Shared.tableCount(GameData.skillData) - Shared.tableCount(advMode.startingSkills)
| |
| local costLength = Shared.tableCount(advMode.skillUnlockCost)
| |
| local returnPart = {}
| |
| table.insert(returnPart, '{| class="wikitable stickyHeader"\r\n|- class="headerRow-0"\r\n!Unlock!!Cost!!Cumulative Cost')
| |
|
| |
| local accCost = 0
| |
| for i = 1, unlockCount, 1 do
| |
| local cost = advMode.skillUnlockCost[math.min(i, costLength)]
| |
| accCost = accCost + cost
| |
| table.insert(returnPart, '|-')
| |
| table.insert(returnPart, '|' .. i .. '||' .. Icons.GP(cost) .. '||' .. Icons.GP(accCost))
| |
| end
| |
| table.insert(returnPart, '|}')
| |
|
| |
| return table.concat(returnPart, '\r\n')
| |
| end
| |
| end
| |
|
| |
| function p.getFiremakingTable(frame)
| |
| local resultPart = {}
| |
| table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
| |
| 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, '!!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!XP!!XP/s!!XP!!XP/s')
| |
|
| |
| for i, logData in ipairs(SkillData.Firemaking.logs) do
| |
| local logs = Items.getItemByID(logData.logID)
| |
| local name = logs.name
| |
| local burnTime = logData.baseInterval / 1000
| |
| local bonfireTime = logData.baseBonfireInterval / 1000
| |
| local XPS = logData.baseExperience / burnTime
| |
| local XP_BF = logData.baseExperience * (1 + logData.bonfireXPBonus / 100)
| |
| local XPS_BF = Shared.round(XP_BF / burnTime, 2, 2)
| |
| XP_BF = Shared.round(XP_BF, 2, 0)
| |
|
| |
| table.insert(resultPart, '\r\n|-')
| |
| 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, '||style ="text-align: right;"|'..logData.level)
| |
| 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="'..XPS..'"|'..Shared.formatnum(Shared.round(XPS, 2, 2)))
| |
| 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="'..logData.bonfireXPBonus..'"|'..logData.bonfireXPBonus..'%')
| |
| table.insert(resultPart, '||style ="text-align: right;" data-sort-value="'..bonfireTime..'"|'..Shared.timeString(bonfireTime, true))
| |
| end
| |
|
| |
| table.insert(resultPart, '\r\n|}')
| |
| return table.concat(resultPart)
| |
| end
| |
|
| |
| return p
| |