Module:SkillUnlocks

From Melvor Idle
Revision as of 00:07, 24 April 2023 by Pksage (talk | contribs)

Documentation for this module may be created at Module:SkillUnlocks/doc

local p = {}

--This module polls various game data sources to produce a full list of skill
--level unlocks for each skill. The game has a hard-coded set of "milestones"
--for each skill that does the same thing, but it is not exhaustive and not
--permanently visible for most combat skills.

local Shared = require('Module:Shared')
local Constants = require('Module:Constants')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local CombatAreas = require('Module:CombatAreas')

local CHECK_ITEMS = {'Attack', 'Strength', 'Defence', 'Ranged', 'Magic', 
	'Slayer'}
local CHECK_AREAS = {'Slayer'}
local TYPE_SORT_ORDER = {
	['item'] = 1, 
	['combatArea'] = 2,
	['slayerArea'] = 3,
	['dungeon'] = 4
}
local VERBS_PER_SUBTYPE = {
	['Weapon'] = 'Wield',
	['Magic Wand'] = 'Wield',
	['Magic Staff'] = 'Wield',
	['Magic Book'] = 'Wield',
	['Armour'] = 'Wear',
	['Magic Armour'] = 'Wear',
	['Ranged Armour'] = 'Wear',
	['Slayer Armour'] = 'Wear',
	['Ring'] = 'Wear', 
	['Amulet'] = 'Wear',
	['combatArea'] = 'Access',
	['slayerArea'] = 'Access',
	['dungeon'] = 'Access'
}
local SUBTYPE_OVERRIDES = {
	['FrostSpark 1H Sword'] = 'Weapon',
	['Lightning Strike 1H Sword'] = 'Weapon',
	['Royal Toxins Spear'] = 'Weapon',
	['Spectral Ice Sword'] = 'Weapon',
	['Ethereal Greataxe'] = 'Weapon',
	['Agile Wings Rapier'] = 'Weapon'
}

function p._getEntityTrueSubtype(entityType, entityName)
	-- Corrects types of a few entities with misleading/wrong data
	if SUBTYPE_OVERRIDES[entityName] then
		entityType = SUBTYPE_OVERRIDES[entityName]
	end
	return entityType
end

function p._addItemsWithSkillRequirement(entityList, skillName)
	local skillReqLabel = skillName:lower() .. 'LevelRequired'
	local itemList = Items.getItems(function(item)
		-- Exclude Golbin Raid exclusives
		if item.golbinRaidExclusive ~= nil and item.golbinRaidExclusive then
			return false
		end
		
		return Items._getItemStat(item, skillReqLabel) ~= nil
	end)
	
	-- TODO: This can probably be made into a generic function for each entity
	for i, item in ipairs(itemList) do
		local processed = {}
		processed.entity = item
		processed.entityName = item.name
		processed.entityType = 'item'
		processed.subType = p._getEntityTrueSubtype(item.type, item.name)
		processed.skillLevel = Items._getItemStat(item, skillReqLabel)
		table.insert(entityList, processed)
	end
	
	return entityList
end

-- This covers combat areas, Slayer areas, and dungeons
function p._addAreasWithSkillRequirement(entityList, skillName)
	local areaList = CombatAreas.getAreas(function(area)
		local hasSkillReq = false
		for i, req in ipairs(area.entryRequirements) do
			if req.type == 'SkillLevel' and req.skillID == Constants.getSkillID(skillName) then
				hasSkillReq = true
			end
		end
		return hasSkillReq
	end)
	
	for i, area in ipairs(areaList) do
		local processed = {}
		processed.entity = area
		processed.entityName = area.name
		processed.entityType = area.type
		processed.subType = p._getEntityTrueSubtype(area.type, area.name)
		for a, req in ipairs(area.entryRequirements) do
			if req.type == 'SkillLevel' and req.skillID == Constants.getSkillID(skillName) then
				processed.skillLevel = req.level
			end
		end
		table.insert(entityList, processed)
	end
	
	return entityList
end

function p._getSkillUnlockTable(skillName)
	-- local skillReqLabel = skillName:lower() .. 'LevelRequired'
	
	-- What do we need to check for this skill? (avoid checking everything for
	-- every skill to save time)
	local entityList = {}
	if Shared.contains(CHECK_ITEMS, skillName) then
		entityList = p._addItemsWithSkillRequirement(entityList, skillName)
	end
	if Shared.contains(CHECK_AREAS, skillName) then
		entityList = p._addAreasWithSkillRequirement(entityList, skillName)
	end
	
	if Shared.tableIsEmpty(entityList) then
		-- TODO: More specific empty handling
		return nil
	end
	
	-- Sort the big list of everything
	table.sort(entityList, function(a, b) 
		-- Sort by level first
		if a.skillLevel ~= b.skillLevel then
			return a.skillLevel < b.skillLevel
		-- Then by type
		elseif TYPE_SORT_ORDER[a.entityType] ~= TYPE_SORT_ORDER[b.entityType] then
			return TYPE_SORT_ORDER[a.entityType] < TYPE_SORT_ORDER[b.entityType]
		-- And finally by entity name
		else
			return a.entityName < b.entityName
		end
	end)
	
	-- Header and columns
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable"\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!' .. Icons.Icon({skillName, type='skill', notext='true'}) .. ' Level')
	table.insert(resultPart, '\r\n!Unlocks')
	
	-- Time to iterate!
	local currentLevel = 0
	for i, entity in ipairs(entityList) do
		local foundLevel = entity.skillLevel
		
		-- Start a new row if the current entity's level is higher than the
		-- current row
		if foundLevel ~= currentLevel then
			currentLevel = foundLevel
			table.insert(resultPart, '\r\n|-')
			table.insert(resultPart, '\r\n|' .. foundLevel)
			table.insert(resultPart, '\r\n|')
		else
			table.insert(resultPart, '<br/>')
		end
		
		-- What are you doing with the thing you unlock? ("verbs")
		local verb = ''
		if VERBS_PER_SUBTYPE[entity.subType] then
			verb = VERBS_PER_SUBTYPE[entity.subType] .. ' '
		end
		
		-- Icon overrides
		local iconType = entity.entityType
		if entity.entityType == 'slayerArea' then
			iconType = 'combatArea'
		end
		
		-- Append entity to the column
		table.insert(resultPart, verb .. Icons.Icon({entity.entityName, type=iconType}))
	end
	
	table.insert(resultPart, '\r\n|}')

	return table.concat(resultPart)
end

function p.getSkillUnlockTable(frame)
	local skillName = frame.args ~= nil and frame.args[1]
	return p._getSkillUnlockTable(skillName)
end

return p