Module:Modifiers: Difference between revisions

From Melvor Idle
(Support v1.3 modifiers)
 
(Amend error handling)
Line 518: Line 518:
local modNS, modLocalID = Shared.getLocalID(modID)
local modNS, modLocalID = Shared.getLocalID(modID)
local modDefn = p.getModifierByID(modLocalID)
local modDefn = p.getModifierByID(modLocalID)
if modDefn == nil then
return Shared.printError('No modifier found with ID: ' .. (modLocalID or 'nil'))
end
local scopeDefn = p.getScope(modDefn, scopeData)
local scopeDefn = p.getScope(modDefn, scopeData)
local descTemplate = p.getDescriptionTemplate(scopeDefn, scopeData)
local descTemplate = p.getDescriptionTemplate(scopeDefn, scopeData)

Revision as of 08:17, 25 June 2024

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

-- Module responsible for handling modifiers

local p = {}

local GameData = require('Module:GameData')
local Shared = require('Module:Shared')
local Common = require('Module:Common')

-- Initialisation
-- Index modifier definitions by their aliases & local IDs
p.ModifierIndex = {
	["alias"] = {},
	["localID"] = {}
}
for _, modDefn in ipairs(GameData.rawData.modifiers) do
	-- Index by local ID
	local ns, modLocalID = Shared.getLocalID(modDefn.id)
	p.ModifierIndex.localID[modLocalID] = modDefn
	-- Index by alias
	for _, allowedScope in ipairs(modDefn.allowedScopes) do
		local aliasKeys = {
			{ ["key"] = 'posAliases', ["type"] = 'pos' },
			{ ["key"] = 'negAliases', ["type"] = 'neg' }
		}
		for _, keyDefn in ipairs(aliasKeys) do
			local modAliases = allowedScope[keyDefn.key]
			if modAliases ~= nil then
				for _, modAlias in ipairs(modAliases) do
					if modAlias.key ~= nil then
						p.ModifierIndex.alias[modAlias.key] = {
							['scope'] = allowedScope,
							['mod'] = modDefn,
							['alias'] = modAlias,
							['type'] = keyDefn.type
						}
					end
				end
			end
		end
	end
end

-- Maps object modifier keys to modifier definition scope keys
-- See in game code: class ModifierScope, static getScopeFromData
local ScopeKeyMap = {
	['skillID'] = {
		['key'] = 'skill',
		['templateKey'] = 'skillName'
	},
	['damageTypeID'] = {
		['key'] = 'damageType',
		['templateKey'] = 'damageType',
		['templateKey2'] = 'resistanceName'
	},
	['realmID'] = {
		['key'] = 'realm',
		['templateKey'] = 'realmName'
	},
	['currencyID'] = {
		['key'] = 'currency',
		['templateKey'] = 'currencyName'
	},
	['categoryID'] = {
		['key'] = 'category',
		['templateKey'] = 'categoryName'
	},
	['actionID'] = {
		['key'] = 'action',
		['templateKey'] = 'actionName'
	},
	['subcategoryID'] = {
		['key'] = 'subcategory',
		['templateKey'] = 'subcategoryName'
	},
	['itemID'] = {
		['key'] = 'item',
		['templateKey'] = 'itemName'
	},
	['effectGroupID'] = {
		['key'] = 'effectGroup',
		['templateKey'] = 'effectGroupName'
	},
}

-- Retrieves a modifier definition by ID
function p.getModifierByID(modID)
	local modNS, modLocalID = Shared.getLocalID(modID)
	return p.ModifierIndex.localID[modLocalID]
end

-- Given a modifier alias, returns the relevant modifier and scope definitions
function p.getModifierByAlias(modAlias)
	return p.ModifierIndex.alias[modAlias]
end

-- Given a modifier's scope data, converts it to a scope definition that can be used
-- to match against another scope definition
function p.convertScopeDataToDefinition(scopeData)
	local scopeDefn = {}
	if scopeData ~= nil then
		for k, v in pairs(scopeData) do
			if k ~= 'value' then
				local scopeKey = ScopeKeyMap[k]
				if scopeKey == nil then
					error('Failed to map scope data with key "' .. k .. '" to a scope key', 2)
				end
				scopeDefn[scopeKey.key] = true
			end
		end
	end
	return scopeDefn
end

-- Compares two scope definitions to check if they are equal
function p.doScopeDefinitionsMatch(scopeDefn1, scopeDefn2)
	if Shared.tableCount(scopeDefn1) ~= Shared.tableCount(scopeDefn2) then
		return false
	end
	-- If sizes match, compare elements of each definition
	for k, v in pairs(scopeDefn1) do
		if not scopeDefn2[k] then
			-- Definition 1 has a key that definition 2 does not
			return false
		end
	end
	-- At this point, both definitions have the same elements
	return true
end

-- Checks that the given scope data matches the restrictions imposed by dataCriteria.
-- This is used to check that a modifier provides its benefit to a given skill, or currency,
-- or so on.
-- dataCriteria accepts one additional property that isn't usually found in scope data, being
-- "valueType". This indicates whether the value of the modifier should be positive ('pos') or
-- negative ('neg') in order for the criteria to be met
function p.checkScopeDataMeetsCriteria(scopeData, scopeDefn, dataCriteria)
	for criteriaKey, criteriaValue in pairs(dataCriteria) do
		if criteriaKey == 'valueType' then
			-- Special case, check sign of modifier value
			if (
				(criteriaValue ~= nil and scopeData.value ~= nil)
				and not (
					(criteriaValue == 'pos' and scopeData.value > 0)
					or (criteriaValue == 'neg' and scopeData.value < 0)
		 		)
		 	) then
				-- Value criteria not met
				return false
			end
		elseif criteriaValue ~= nil and criteriaKey ~= 'key' then
			local mapData = ScopeKeyMap[criteriaKey]
			if mapData == nil then
				error('Invalid modifier scope data criteria: ' .. criteriaKey, 2)
			end
			local defnKey = mapData.key
			if (
				-- Scope definition indicates the criteria is used for this modifier
				scopeDefn[defnKey]
				and (
					scopeData[criteriaKey] == nil
					or scopeData[criteriaKey] ~= criteriaValue
				)	
			) then
				-- Modifier value doesn't match criteria for this key
				return false
			end
		end
	end
	-- All criteria checked successfully
	return true
end

-- Given a list of modifier IDs and aliases, returns all match criteria for these.
-- This matching criteria can then be pased to p.getMatchingModifiers()
function p.getMatchCriteriaFromIDs(modifierIDs, modifierAliases)
	local matchCriteria = {}
	-- For modifier IDs, find the relevant mod definition and add all allowed scopes
	if modifierIDs ~= nil then
		for _, modifierID in ipairs(modifierIDs) do
			local modDefn = p.getModifierByID(modifierID)
			if modDefn == nil then
				error('No such modifier ID: ' .. modifierID)
			end

			for _, allowedScope in ipairs(modDefn.allowedScopes) do
				if Shared.tableIsEmpty(allowedScope.scopes) then
					-- No scope definitions, so add modifier once with an empty scope
					table.insert(matchCriteria, {
						["mod"] = modDefn,
						["scope"] = {}
					})
				else
					-- Add all available scope definitions
					for _, scopeDefn in ipairs(allowedScope.scopes) do
						table.insert(matchCriteria, {
							["mod"] = modDefn,
							["scope"] = scopeDefn
						})
					end
				end
			end
		end
	end
	-- For alias IDs, simply add these one at a time to the table
	if modifierAliases ~= nil then
		for _, modifierAlias in ipairs(modifierAliases) do
			table.insert(matchCriteria, {
				["alias"] = modifierAlias
			})
		end
	end
	return matchCriteria
end

-- Checks if any elements of modifiers match the provided criteria, and
-- returns any elements which do match.
-- skillID when specified ensures that any skill specific modifiers are only matched
-- when they relate to that given skill ID
-- matchCriteria is a table of elements structured as follows:
-- { ["mod"] = modDefn, ["scope"] = scopeDefn, ["alias"] = modAlias }
-- Examples of valid mod and scope definitions can be obtained from
-- p.getModifierByID() and p.getScope()
-- alias is an optional property, if specified mod and scope are derived
-- from the modAlias.
function p.getMatchingModifiers(modifiers, matchCriteria, skillID)
	local resultMods = {
		["matched"] = {},
		["unmatched"] = {}
	}
	if type(matchCriteria) == 'table' and not Shared.tableIsEmpty(matchCriteria) then
		-- Transform matchCriteria
		local matchCriteriaMap = {}
		for _, matchDefn in ipairs(matchCriteria) do
			local modDefn, scopeDefn, modAlias, valueType = matchDefn.mod, matchDefn.scope, {}, matchDefn.type
			if matchDefn.alias ~= nil then
				local aliasData = p.getModifierByAlias(matchDefn.alias)
				if aliasData == nil then
					error('No such modifier alias: ' .. matchDefn.alias, 2)
				else
					modDefn, scopeDefn, modAlias, valueType = aliasData.mod, aliasData.scope.scopes, aliasData.alias, aliasData.type
					
				end
			end
			local modNS, modLocalID = Shared.getLocalID(modDefn.id)
			if matchCriteriaMap[modLocalID] == nil then
				matchCriteriaMap[modLocalID] = {}
			end
			table.insert(matchCriteriaMap[modLocalID], { ["scope"] = scopeDefn, ["alias"] = modAlias, ["valueType"] = valueType })
		end

		local function addToResult(category, modID, scopeData)
			if resultMods[category][modID] == nil then
				resultMods[category][modID] = {}
			end
			table.insert(resultMods[category][modID], scopeData)
		end

		for modID, scopeDataRaw in pairs(modifiers) do
			-- modID can sometimes be namespaced
			local modNS, modLocalID = Shared.getLocalID(modID)
			local scopeDataArray = p.getIteratableScopeData(scopeDataRaw)
			-- Find any match criteria relevant to this modifier
			local modMatchCriteria = matchCriteriaMap[modLocalID]
			if modMatchCriteria ~= nil then
				-- Check scope data matches
				for _, scopeData in ipairs(scopeDataArray) do
					local isProcessed = false
					local modScopeDefn = p.convertScopeDataToDefinition(scopeData)
					for _, matchDefn in ipairs(modMatchCriteria) do
						local scopeDefn, modAlias = matchDefn.scope, matchDefn.alias
						local dataCriteria = {
							["skillID"] = skillID,
							["valueType"] = matchDefn.valueType
						}
						local matchKey = 'unmatched'
						-- Check that:
						--  - scope definitions match
						--  - Data criteria from the alias (within modAlias) are met
						--  - Data criteria from elsewhere (within dataCriteria) are met
						if (
							p.doScopeDefinitionsMatch(modScopeDefn, scopeDefn)
							and p.checkScopeDataMeetsCriteria(scopeData, scopeDefn, modAlias)
							and p.checkScopeDataMeetsCriteria(scopeData, scopeDefn, dataCriteria)
					 	) then
							-- Add to matched table
							addToResult('matched', modID, scopeData)
							isProcessed = true
							break
						end
					end
					if not isProcessed then
						addToResult('unmatched', modID, scopeData)
					end
				end
			else
				for _, scopeData in ipairs(scopeDataArray) do
					addToResult('unmatched', modID, scopeData)
				end
			end
		end
	end
	return resultMods
end

-- Given a table of modifiers, returns the cumulated magnitudes of those modifiers
function p.getModifierValue(modifiers)
	local modValue = 0
	for modID, scopeDataRaw in pairs(modifiers) do
		local scopeDataArray = p.getIteratableScopeData(scopeDataRaw)
		for _, scopeData in ipairs(scopeDataArray) do
			if type(scopeData.value) == 'number' then
				modValue = modValue + scopeData.value
			end
		end
	end
	return modValue
end

-- Given a modifier definition & scope data, returns the appropriate scope definition
function p.getScope(modDefn, scopeData)
	-- First derive the desired scope definition that matches the provided data
	local wantedScopeDefn = p.convertScopeDataToDefinition(scopeData)

	-- Attempt to find an allowed scope with a matching definition
	for _, allowedScope in ipairs(modDefn.allowedScopes) do
		if p.doScopeDefinitionsMatch(wantedScopeDefn, allowedScope.scopes) then
			return allowedScope
		end
	end
	-- No scope matches at this point
	return nil
end

-- Given a scope definition & scope data, returns the appropriate description template
function p.getDescriptionTemplate(scopeDefn, scopeData)
	local dataValue = scopeData.value
	for _, descTemplate in ipairs(scopeDefn.descriptions) do
		local condAbove, condBelow = descTemplate.above, descTemplate.below
		if (
			condAbove == nil
			or (condAbove ~= nil and dataValue ~= nil and dataValue > condAbove)
		) and (
			condBelow == nil
			or (condBelow ~= nil and dataValue ~= nil and dataValue < condBelow)
		) then
			return descTemplate
		end
	end
end

-- Given a value and a modification rule, returns a modified value 
function p.modifyValue(value, modifyRule)
	if modifyRule == 'value*hpMultiplier' then
		return value * 10
	elseif modifyRule == 'value/hpMultiplier' then
		return value / 10
	elseif modifyRule == 'value/1000' then
		return value / 1000
	elseif modifyRule == '2^value' then
		return 2^value
	elseif modifyRule == '100 + value' then
		return 100 + value
	elseif modifyRule == 'value*100' then
		return value * 100
	elseif modifyRule == 'floor(value)' then
		return math.floor(value)
	else
		error('Unknown value modification rule: ' .. (modifyRule or 'nil'), 2)
	end
end

-- Given scope data and optionally a mod definition & description template, returns template data
-- that can be used with Shared.applyTemplateData() to retrieve a modifier description.
-- descriptionTemplate is used to determine if the value should be signed or not (default
-- is unsigned)
-- modDefn is used to determine if the value should be modified or not (default is unmodified)
function p.getTemplateData(scopeData, modDefn, scopeDefn, descriptionTemplate)
	local templateData = {}
	local signValue = false
	local modifyRule = nil
	if descriptionTemplate ~= nil then
		signValue = (descriptionTemplate.includeSign == nil or descriptionTemplate.includeSign)
	end
	if modDefn ~= nil and modDefn.modifyValue ~= nil then
		modifyRule = modDefn.modifyValue
	end
	-- Some scopes have source data, being many skill related modifiers and some modifiers which
	-- affect certain categories of spells
	local scopeSourceID, scopeSourceData = scopeDefn.scopeSource or scopeData.skillID, nil
	if scopeSourceID == 'AttackSpell' then
		-- Uses spell categories, contained within magic skill data
		scopeSourceData = GameData.skillData.Magic
	elseif scopeSourceID ~= nil then
		-- Assumed to be a skill ID
		local skillObj = GameData.getEntityByProperty('skillData', 'skillID', scopeSourceID)
		if skillObj == nil then
			error('Unhandled scope source ID: ' .. scopeSourceID)
		else
			scopeSourceData = skillObj.data
		end
	end
	for k, v in pairs(scopeData) do
		local tKey, tVal = k, v
		if tKey == 'value' then
			if modifyRule ~= nil then
				tVal = p.modifyValue(tVal, modifyRule)
			end
			templateData.valueUnsigned = tVal
			if signValue then
				tVal = (tVal >= 0 and '+' or '-') .. math.abs(tVal)
			else
				tVal = math.abs(tVal)
			end
		else
			local keyMap = ScopeKeyMap[k]
			if keyMap == nil then
				error('Failed to map scope data with key "' .. tKey .. '" to a scope key', 2)
			end
			tKey = keyMap.templateKey
			if tKey == 'skillName' and scopeSourceData ~= nil then
				tVal = scopeSourceData.name
			elseif tKey == 'damageType' then
				local damageType = GameData.getEntityByID('damageTypes', v)
				tVal = (damageType ~= nil and damageType.name) or Shared.printError('Unknown damage type: ' .. v)
				local tKey2 = keyMap.templateKey2
				templateData[tKey2] = (damageType ~= nil and damageType.resistanceName) or tVal
			elseif tKey == 'realmName' then
				local realm = GameData.getEntityByID('realms', v)
				tVal = (realm ~= nil and realm.name) or Shared.printError('Unknown realm: ' .. v)
			elseif tKey == 'currencyName' then
				local currency = GameData.getEntityByID('currencies', v)
				tVal = (currency ~= nil and currency.name) or Shared.printError('Unknown currency: ' .. v)
			elseif tKey == 'categoryName' then
				if scopeSourceData == nil then
					error('Skill data is required for scope type ' .. tKey, 2)
				end
				local catKey = (
					(scopeSourceID == 'AttackSpell' and 'spellCategories')
					or (scopeSourceID == 'melvorD:Thieving' and 'areas')
					or (scopeSourceID == 'melvorD:Township' and 'biomes')
					or 'categories'
				)
				if scopeSourceData[catKey] == nil then
					error('No categories property for scope source with ID ' .. scopeSourceID .. ', ' .. v, 2)
				end
				local category = GameData.getEntityByID(scopeSourceData[catKey], v)
				tVal = (category ~= nil and (category.name or v)) or Shared.printError('Unknown category: ' .. v)
			elseif tKey == 'actionName' then
				if scopeSourceData == nil then
					error('Skill data is required for scope type ' .. tKey, 2)
				end
				local recipeKey = (
					(scopeSourceID == 'melvorD:Township' and 'resources')
					or Common.getSkillRecipeKey(scopeSourceID)
				)

				local action = GameData.getEntityByID(scopeSourceData[recipeKey], v)
				if action == nil then
					tVal = Shared.printError('Unknown action: ' .. v, ' for scope source ID ' .. scopeSourceID)
				else
					local actionName = action.name
					if actionName == nil and action.productID ~= nil then
						local productItem = GameData.getEntityByID('items', action.productID)
						actionName = productItem.name
					end
					tVal = (action ~= nil and (actionName or v)) or Shared.printError('Unknown action: ' .. v, ' for scope source ID ' .. scopeSourceID)
				end
			elseif tKey == 'subcategoryName' then
				local subcategory = nil
				if scopeSourceData == nil then
					error('Scope source data is required for scope type ' .. tKey, 2)
				elseif scopeSourceID == 'AttackSpell' then
					subcategory = GameData.getEntityByID(scopeSourceData.spellCategories, v)
				else
					subcategory = GameData.getEntityByID(scopeSourceData.subcategories, v)
				end
				tVal = (subcategory ~= nil and subcategory.name) or Shared.printError('Unknown subcategory: ' .. v)
			elseif tKey == 'itemName' then
				local item = GameData.getEntityByID('items', v)
				tVal = (item ~= nil and item.name) or Shared.printError('Unknown item: ' .. v)
			elseif tKey == 'effectGroupName' then
				local effectGroup = GameData.getEntityByID('combatEffectGroups', v)
				tVal = (effectGroup ~= nil and effectGroup.name) or Shared.printError('Unknown effect group: ' .. v)
			else
				error('Failed to convert scope type ' .. tKey .. ' to description', 2)
			end
		end
		templateData[tKey] = tVal
	end
	return templateData
end

-- Given the scope data assigned to a modifier, transforms it into a format that can be iterated
function p.getIteratableScopeData(scopeData)
	-- scopeData may be a table of multiple scope data sets, which will appear as
	-- multiple rows. Split these so that the number of visible lines is always accurate
	if type(scopeData) == 'table' and scopeData[1] ~= nil then
		-- Elements of the table will have numeric indexes if a table of multiple
		-- scope data sets
		return scopeData
	elseif type(scopeData) ~= 'table' then
		-- Single (likely numeric) value
		return { { ["value"] = scopeData } }
	else
		-- Data is a table, but appears to be a single scope data set
		return {scopeData}
	end
end

-- Given a modifier local ID and scope data, returns a modifier description.
-- applyColour is optional, colouring the modifier green or red to indicate a
-- positive or negative impact upon the player respectively
function p.getModifierText(modID, scopeData, applyColour)
	if applyColour == nil then
		applyColour = true
	end
	-- Sometimes ID can be namespaced
	local modNS, modLocalID = Shared.getLocalID(modID)
	local modDefn = p.getModifierByID(modLocalID)
	if modDefn == nil then
		return Shared.printError('No modifier found with ID: ' .. (modLocalID or 'nil'))
	end
	local scopeDefn = p.getScope(modDefn, scopeData)
	local descTemplate = p.getDescriptionTemplate(scopeDefn, scopeData)
	local templateData = p.getTemplateData(scopeData, modDefn, scopeDefn, descTemplate)
	local modTextPlain = Shared.applyTemplateData(descTemplate.text, templateData)
	local valU = templateData.valueUnsigned
	if applyColour and valU ~= nil then
		local isInverted = modDefn.inverted
		if isInverted == nil then
			isInverted = false
		end
		local isPositive = ((isInverted and valU < 0) or (not isInverted and valU > 0))
		local colourClass = isPositive and 'text-positive' or 'text-negative'
		return '<span class="' .. colourClass .. '">' .. modTextPlain .. '</span>'
	end
	return modTextPlain
end

-- Given a table of modifiers (kv pairs, k = mod local ID, v = scope data), 
-- produces text including all modifier descriptions.
-- applyColour is optional, colouring the modifier green or red to indicate a
-- positive or negative impact upon the player respectively.
-- inline is optional, if specified as true, the returned modifiers are presented
-- upon a single line, as opposed to one modifier per line.
-- maxVisible is optional, if specified as an integer, any modifiers beyond the
-- limit this parameter imposes are hidden by default
-- entryDecorator is optional, if specified this is a function which will accept 
-- text for each individual modifier entry, and will return the desired format of
-- that entry.
-- entrySeparator is optional, if specified as a string, this defines the text separator
-- for each modifier entry
function p.getModifiersText(modifiers, applyColour, inline, maxVisible, entryDecorator, entrySeparator)
	if modifiers == nil or Shared.tableIsEmpty(modifiers) then
		return ''
	end
	if inline == nil then
		inline = false
	end
	if type(maxVisible) ~= 'number' then
		maxVisible = nil
	end
	local entrySep = nil
	if type(entrySeparator) == 'string' then
		entrySep = entrySeparator
	else
		entrySep = (inline and ' and ' or '<br>')
	end
	local function formatLine(text)
		if entryDecorator == nil then
			return text
		else
			return entryDecorator(text)
		end
	end

	local modArray = { ["visible"] = {}, ["overflow"] = {} }
	local modCount = { ["visible"] = 0, ["overflow"] = 0 }
	local insertKey = 'visible'
	for modLocalID, scopeDataRaw in pairs(modifiers) do
		-- scopeDataRaw may be a table of multiple scope data sets, which will appear as
		-- multiple rows. Split these so that the number of visible lines is always accurate
		local scopeDataArray = p.getIteratableScopeData(scopeDataRaw)
		for _, scopeData in ipairs(scopeDataArray) do
			if (maxVisible ~= nil and not inline and insertKey == 'visible'
				and modCount[insertKey] >= maxVisible) then
				insertKey = 'overflow'
			end
			table.insert(modArray[insertKey], formatLine(p.getModifierText(modLocalID, scopeData, applyColour)))
			modCount[insertKey] = modCount[insertKey] + 1
		end
	end

	if modCount.overflow == 1 then
		-- Having a single toggle-able line occupies the same height as showing all modifiers
		table.insert(modArray.visible, modArray.overflow[1])
	end
	local overflowText = ''
	if modCount.overflow > 1 then
		-- Number of other modifiers has exceeded the specified maximum
		overflowText = table.concat({
			'<br><span class="mw-collapsible mw-collapsed" data-expandtext=',
			'"Show ' .. Shared.formatnum(modCount.overflow) .. ' more modifiers" ',
			'data-collapsetext="Hide">',
			table.concat(modArray.overflow, entrySep),
			'</span>'
		})
	end
	return table.concat(modArray.visible, entrySep) .. overflowText
end

-- Given a table of modifiers (kv pairs, k = mod local ID, v = scope data),
-- returns a table of skill names involved for any modifiers affecting specific skills
function p.getModifierSkills(modifiers)
	local skillArray = {}
	for localModID, scopeDataRaw in pairs(modifiers) do
		local scopeDataArray = p.getIteratableScopeData(scopeDataRaw)
		for _, scopeData in ipairs(scopeDataArray) do
			local skillID = scopeData.skillID
			if skillID ~= nil and not Shared.contains(skillArray, skillID) then
				table.insert(skillArray, skillID)
			end
		end
	end

	local skillNameArray = {}
	for _, skillID in ipairs(skillArray) do
		local skillName = GameData.getEntityByProperty('skillData', 'skillID', skillID).data.name
		table.insert(skillNameArray, skillName)
	end
	return skillNameArray
end

-- Leaving for now as examples of usage
function p.test()
	local modDefn = p.getModifierByID('skillCostReduction')
	local scopeData = {
		["realmID"] = 'melvorItA:Abyssal',
		["value"] = 5
	}
	local scopeDefn = p.getScope(modDefn, scopeData)
	local descTemplate = p.getDescriptionTemplate(scopeDefn, scopeData)
	local templateData = p.getTemplateData(scopeData, modDefn, scopeDefn, descTemplate)
	return p.getModifierText('skillCostReduction', scopeData, true)
end

function p.test2()
	local item = GameData.getEntityByID('items', 'melvorF:Miners_Helmet') --'melvorD:Aorpheats_Signet_Ring')
	return p.getModifiersText(item.modifiers, true, false, 5)
	--return p.getModifierSkills(item.modifiers)
end

function p.test3()
	local item = GameData.getEntityByID('items', 'melvorD:Aorpheats_Signet_Ring')
	local matchCriteria = {
		{
			--["mod"] = p.getModifierByID('skillItemDoublingChance'),
			--["scope"] = {
			--	["skill"] = true
			--}
			-- Should no longer match if changed to increased
			["alias"] = 'decreasedChanceToDoubleItemsSkill',
		},
		{
			["mod"] = p.getModifierByID('globalItemDoublingChance'),
			["scope"] = {}
		},
		{
			-- Should only match the bonus for thieving
			["mod"] = p.getModifierByID('currencyGain'),
			["scope"] = {
				["currency"] = true,
				["skill"] = true
			}
		},
		{
			["mod"] = p.getModifierByID('combatLootDoublingChance'),
			["scope"] = {
				-- Shouldn't match because of mis-matching scope definition
				["damageType"] = true
			}
		}
	}
	return p.getMatchingModifiers(item.modifiers, matchCriteria, 'melvorD:Thieving')
end

function p.test4()
	local purch = GameData.getEntityByID('shopPurchases', 'melvorD:Rune_Axe')
	local matchCriteria = {
		{
			["alias"] = 'decreasedSkillIntervalPercent'
		}
	}
	local matchedMods = p.getMatchingModifiers(purch.contains.modifiers, matchCriteria)
	mw.logObject(matchedMods)
	return p.getModifierValue(matchedMods.matched)
	--return p.getMatchingModifiers(purch.contains.modifiers, matchCriteria)
	
end

return p