Module:Sandbox/Modifiers
Documentation for this module may be created at Module:Sandbox/Modifiers/doc
-- Module responsible for handling modifiers
local p = {}
local GameData = require('Module:Sandbox/GameData')
local Shared = require('Module:Shared')
-- 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 = { 'posAliases', 'negAliases' }
for _, key in ipairs(aliasKeys) do
local modAliases = allowedScope[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
}
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 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 = {}
local wantedScopeSize = 0
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
wantedScopeDefn[scopeKey.key] = true
wantedScopeSize = wantedScopeSize + 1
end
end
end
-- Attempt to find an allowed scope with a matching definition
for _, allowedScope in ipairs(modDefn.allowedScopes) do
local scopeMatched = true
local scopeSize = 0
for k, v in pairs(allowedScope.scopes) do
if not wantedScopeDefn[k] then
-- The scope definition has a key that isn't wanted, don't match
scopeMatched = false
break
end
scopeSize = scopeSize + 1
end
if scopeMatched and scopeSize == wantedScopeSize then
-- Scope matches
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
-- TODO Lazy copy from Module:Skills to avoid dependency, must resolve later
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 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, descriptionTemplate)
local templateData = {}
local signValue = false
local modifyRule = nil
if descriptionTemplate ~= nil and descriptionTemplate.includeSign ~= nil then
signValue = (descriptionTemplate.includeSign == "true")
end
if modDefn ~= nil and modDefn.modifyValue ~= nil then
modifyRule = modDefn.modifyValue
end
-- If scope data has a skill ID, retrieve the skill data
local skillData = nil
if scopeData.skillID ~= nil then
skillData = GameData.getEntityByProperty('skillData', 'skillID', scopeData.skillID).data
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 skillData ~= nil then
tVal = skillData.name
elseif tKey == 'damageType' then
local damageType = GameData.getEntityByID('damageTypes', v)
tVal = damageType.name
local tKey2 = keyMap.templateKey2
templateData[tKey2] = damageType.resistanceName
elseif tKey == 'realmName' then
tVal = GameData.getEntityByID('realms', v).name
elseif tKey == 'currencyName' then
tVal = GameData.getEntityByID('currencies', v).name
elseif tKey == 'actionName' then
if skillData == nil then
error('Skill data is required for scope type ' .. tKey, 2)
end
local recipeKey = p.getSkillRecipeKey(skillData.id)
tVal = GameData.getEntityByID(skillData[recipeKey], v).name
elseif tKey == 'subcategoryName' then
if skillData == nil then
error('Skill data is required for scope type ' .. tKey, 2)
end
tVal = GameData.getEntityByID(skillData.categories, v).name
elseif tKey == 'itemName' then
tVal = GameData.getEntityByID('items', v).name
elseif tKey == 'effectGroupName' then
tVal = GameData.getEntityByID('combatEffectGroups', v).name
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)
local scopeDefn = p.getScope(modDefn, scopeData)
local descTemplate = p.getDescriptionTemplate(scopeDefn, scopeData)
local templateData = p.getTemplateData(scopeData, modDefn, 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
function p.getModifiersText(modifiers, applyColour, inline, maxVisible)
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 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], p.getModifierText(modLocalID, scopeData, applyColour))
modCount[insertKey] = modCount[insertKey] + 1
end
end
if inline then
return table.concat(modArray.visible, ' and ')
else
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, '<br>'),
'</span>'
})
end
return table.concat(modArray.visible, '<br>') .. overflowText
end
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, descTemplate)
return p.getModifierText('skillCostReduction', scopeData, true)
end
function p.test2()
local item = GameData.getEntityByID('items', 'melvorD:Aorpheats_Signet_Ring')
return p.getModifiersText(item.modifiers, true, false, 5)
end
return p