Module:Modifiers: Difference between revisions
From Melvor Idle
(Fix for change in AttackSpell scope source ID) |
(checkScopeDataMeetsCriteria: Support matching the absence of properties with string value 'nil') |
||
(5 intermediate revisions by 2 users not shown) | |||
Line 6: | Line 6: | ||
local Shared = require('Module:Shared') | local Shared = require('Module:Shared') | ||
local Common = require('Module:Common') | local Common = require('Module:Common') | ||
local Num = require('Module:Number') | |||
-- Initialisation | -- Initialisation | ||
Line 32: | Line 33: | ||
['mod'] = modDefn, | ['mod'] = modDefn, | ||
['alias'] = modAlias, | ['alias'] = modAlias, | ||
[' | ['props'] = { ['valueType'] = keyDefn.type } | ||
} | } | ||
end | end | ||
Line 51: | Line 52: | ||
['key'] = 'damageType', | ['key'] = 'damageType', | ||
['templateKey'] = 'damageType', | ['templateKey'] = 'damageType', | ||
['templateKey2'] = 'resistanceName' | ['templateKey2'] = 'resistanceName', | ||
['gameDataKey'] = 'damageTypes' | |||
}, | }, | ||
['realmID'] = { | ['realmID'] = { | ||
['key'] = 'realm', | ['key'] = 'realm', | ||
['templateKey'] = 'realmName' | ['templateKey'] = 'realmName', | ||
['gameDataKey'] = 'realms' | |||
}, | }, | ||
['currencyID'] = { | ['currencyID'] = { | ||
['key'] = 'currency', | ['key'] = 'currency', | ||
['templateKey'] = 'currencyName' | ['templateKey'] = 'currencyName', | ||
['gameDataKey'] = 'currencies' | |||
}, | }, | ||
['categoryID'] = { | ['categoryID'] = { | ||
Line 75: | Line 79: | ||
['itemID'] = { | ['itemID'] = { | ||
['key'] = 'item', | ['key'] = 'item', | ||
['templateKey'] = 'itemName' | ['templateKey'] = 'itemName', | ||
['gameDataKey'] = 'items' | |||
}, | }, | ||
['effectGroupID'] = { | ['effectGroupID'] = { | ||
['key'] = 'effectGroup', | ['key'] = 'effectGroup', | ||
['templateKey'] = 'effectGroupName' | ['templateKey'] = 'effectGroupName', | ||
['gameDataKey'] = 'combatEffectGroups' | |||
}, | }, | ||
} | } | ||
local ScopeKeyToIDMap = {} | |||
for idKey, defn in pairs(ScopeKeyMap) do | |||
ScopeKeyToIDMap[defn.key] = idKey | |||
end | |||
-- Retrieves a modifier definition by ID | -- Retrieves a modifier definition by ID | ||
Line 126: | Line 137: | ||
-- At this point, both definitions have the same elements | -- At this point, both definitions have the same elements | ||
return true | return true | ||
end | |||
-- Given a table containing one or more sets of data criteria, combines those criteria into a | |||
-- single set of criteria. Should the same criteria key be specified more than once, the value from | |||
-- the last set containing that key will be retained | |||
function p.combineDataCriteria(dataCriteriaList) | |||
local rv = {} | |||
for _, dataCriteria in ipairs(dataCriteriaList) do | |||
for dataKey, dataValue in pairs(dataCriteria) do | |||
rv[dataKey] = dataValue | |||
end | |||
end | |||
return rv | |||
end | |||
-- Given data criteria where the values are entity names, converts those names to IDs. | |||
function p.convertCriteriaNamesToIDs(dataCriteriaByName) | |||
local dataCriteria = {} | |||
for criteriaKey, criteriaName in pairs(dataCriteriaByName) do | |||
if criteriaName == nil or criteriaName == '' then | |||
error('Value for criteria ' .. criteriaKey .. ' cannot be nil', 2) | |||
end | |||
if criteriaKey == 'valueType' or criteriaKey == 'modType' then | |||
-- Special cases: | |||
-- valueType restricts by sign of modifier value | |||
-- modType restricts by whether the modifier is deemed to be a positive or | |||
-- negative to the target | |||
dataCriteria[criteriaKey] = string.lower(criteriaName) | |||
elseif ScopeKeyMap[criteriaKey] ~= nil then | |||
-- ID specified directly | |||
dataCriteria[criteriaKey] = criteriaName | |||
else | |||
local criteriaIDKey = ScopeKeyToIDMap[criteriaKey] | |||
if criteriaIDKey == nil then | |||
error('Invalid criteria specified: ' .. criteriaKey, 2) | |||
end | |||
local keyMap = ScopeKeyMap[criteriaIDKey] | |||
local criteriaID = nil | |||
if criteriaName == 'nil' then | |||
-- Special value used to check for the absence of a particular | |||
-- property by p.checkScopeDataMeetsCriteria() - passthrough | |||
criteriaID = criteriaName | |||
elseif keyMap.gameDataKey ~= nil then | |||
local criteriaData = GameData.getEntityByName(keyMap.gameDataKey, criteriaName) | |||
if criteriaData ~= nil then | |||
criteriaID = criteriaData.id | |||
end | |||
elseif criteriaKey == 'skill' then | |||
criteriaID = Common.getSkillID(criteriaName) | |||
else | |||
error('Criteria ' .. criteriaKey .. ' is currently unsupported', 2) | |||
end | |||
if criteriaID == nil then | |||
error('Unknown ' .. criteriaKey .. ': ' .. criteriaName, 2) | |||
end | |||
dataCriteria[criteriaIDKey] = criteriaID | |||
end | |||
end | |||
return dataCriteria | |||
end | end | ||
Line 131: | Line 204: | ||
-- This is used to check that a modifier provides its benefit to a given skill, or currency, | -- This is used to check that a modifier provides its benefit to a given skill, or currency, | ||
-- or so on. | -- or so on. | ||
-- dataCriteria accepts | -- Passing the string 'nil' as a value for any criteria indicates that a match should only | ||
-- | -- occur if the given property is absent from the given scope data | ||
-- negative ('neg') in order for the criteria to be met | -- dataCriteria accepts two additional properties that aren't usually found in scope data, being: | ||
function p.checkScopeDataMeetsCriteria(scopeData, scopeDefn, dataCriteria) | -- - valueType: Indicates whether the value of the modifier should be positive ('pos') or | ||
-- negative ('neg') in order for the criteria to be met | |||
-- - modType: Similar to valueType, except determines whether the modifier is a positive ('pos') | |||
-- or negative ('neg') to the player, rather than simply determining the sign of | |||
-- the value as valueType does | |||
function p.checkScopeDataMeetsCriteria(modDefn, scopeData, scopeDefn, dataCriteria) | |||
for criteriaKey, criteriaValue in pairs(dataCriteria) do | for criteriaKey, criteriaValue in pairs(dataCriteria) do | ||
if criteriaKey == 'valueType' then | if criteriaKey == 'valueType' then | ||
Line 147: | Line 225: | ||
-- Value criteria not met | -- Value criteria not met | ||
return false | return false | ||
end | |||
elseif criteriaKey == 'modType' then | |||
if criteriaValue ~= nil and scopeData.value ~= nil then | |||
local isInverted = modDefn.inverted | |||
if isInverted == nil then | |||
isInverted = false | |||
end | |||
local isPositive = ((isInverted and scopeData.value < 0) or (not isInverted and scopeData.value > 0)) | |||
if not ( | |||
(criteriaValue == 'pos' and isPositive) | |||
or (criteriaValue == 'neg' and not isPositive) | |||
) then | |||
-- Mod type criteria not met | |||
return false | |||
end | |||
end | end | ||
elseif criteriaValue ~= nil and criteriaKey ~= 'key' then | elseif criteriaValue ~= nil and criteriaKey ~= 'key' then | ||
Line 159: | Line 252: | ||
and ( | and ( | ||
scopeData[criteriaKey] == nil | scopeData[criteriaKey] == nil | ||
or scopeData[criteriaKey] == 'nil' | |||
or scopeData[criteriaKey] ~= criteriaValue | or scopeData[criteriaKey] ~= criteriaValue | ||
) | ) | ||
Line 173: | Line 267: | ||
-- Given a list of modifier IDs and aliases, returns all match criteria for these. | -- Given a list of modifier IDs and aliases, returns all match criteria for these. | ||
-- This matching criteria can then be pased to p.getMatchingModifiers() | -- This matching criteria can then be pased to p.getMatchingModifiers() | ||
function p.getMatchCriteriaFromIDs(modifierIDs | function p.getMatchCriteriaFromIDs(modifierIDs) | ||
local matchCriteria = {} | local matchCriteria = {} | ||
-- For modifier IDs, find the relevant mod definition and add all allowed scopes | -- For modifier IDs, find the relevant mod definition and add all allowed scopes | ||
if modifierIDs ~= nil then | if modifierIDs ~= nil then | ||
for _, | for _, idObject in ipairs(modifierIDs) do | ||
local modDefn = p.getModifierByID(modifierID) | local modifierID = idObject.id | ||
local modType = idObject.type | |||
local modProps = idObject.props or {} | |||
if modType == 'id' then | |||
local modDefn = p.getModifierByID(modifierID) | |||
if modDefn == nil then | |||
error('No such modifier ID: ' .. modifierID, 2) | |||
end | |||
-- Add all scopes | |||
for _, allowedScope in ipairs(modDefn.allowedScopes) do | |||
table.insert(matchCriteria, { | table.insert(matchCriteria, { | ||
["mod"] = modDefn, | ["mod"] = modDefn, | ||
["scope"] = {} | ["scope"] = allowedScope.scopes or {}, | ||
["props"] = modProps | |||
}) | }) | ||
end | end | ||
elseif modType == 'alias' then | |||
-- For alias IDs, simply add these one at a time to the table | |||
table.insert(matchCriteria, { | |||
["alias"] = modifierID, | |||
["props"] = modProps | |||
}) | |||
else | |||
error('Unknown modifier ID type: ' .. (modType or 'nil'), 2) | |||
end | end | ||
end | end | ||
end | end | ||
Line 218: | Line 309: | ||
-- when they relate to that given skill ID | -- when they relate to that given skill ID | ||
-- matchCriteria is a table of elements structured as follows: | -- matchCriteria is a table of elements structured as follows: | ||
-- { ["mod"] = modDefn, ["scope"] = scopeDefn, ["alias"] = modAlias } | -- { | ||
-- ["mod"] = modDefn, | |||
-- ["scope"] = scopeDefn, | |||
-- ["alias"] = modAlias, | |||
-- ["props"] = modProps | |||
-- } | |||
-- Examples of valid mod and scope definitions can be obtained from | -- Examples of valid mod and scope definitions can be obtained from | ||
-- p.getModifierByID() and p.getScope() | -- p.getModifierByID() and p.getScope() | ||
-- alias is an optional property, if specified mod and scope are derived | -- alias is an optional property, if specified mod and scope are derived | ||
-- from the modAlias. | -- from the modAlias. | ||
function p.getMatchingModifiers(modifiers, matchCriteria | -- props is an optional property, if specified then a modifier is only matched | ||
-- if the properties (skillID, itemID, etc.) also match. | |||
function p.getMatchingModifiers(modifiers, matchCriteria) | |||
local resultMods = { | local resultMods = { | ||
["matched"] = {}, | ["matched"] = {}, | ||
Line 232: | Line 330: | ||
local matchCriteriaMap = {} | local matchCriteriaMap = {} | ||
for _, matchDefn in ipairs(matchCriteria) do | for _, matchDefn in ipairs(matchCriteria) do | ||
local modDefn, scopeDefn, modAlias, | local modDefn, scopeDefn, modAlias, propCriteria = matchDefn.mod, matchDefn.scope, {}, (matchDefn.props or {}) | ||
if matchDefn.alias ~= nil then | if matchDefn.alias ~= nil then | ||
local aliasData = p.getModifierByAlias(matchDefn.alias) | local aliasData = p.getModifierByAlias(matchDefn.alias) | ||
Line 238: | Line 336: | ||
error('No such modifier alias: ' .. matchDefn.alias, 2) | error('No such modifier alias: ' .. matchDefn.alias, 2) | ||
else | else | ||
modDefn, scopeDefn, modAlias, | local aliasProps = p.combineDataCriteria({propCriteria, aliasData.props}) | ||
modDefn, scopeDefn, modAlias, propCriteria = aliasData.mod, aliasData.scope.scopes, aliasData.alias, aliasProps | |||
end | end | ||
Line 246: | Line 345: | ||
matchCriteriaMap[modLocalID] = {} | matchCriteriaMap[modLocalID] = {} | ||
end | end | ||
table.insert(matchCriteriaMap[modLocalID], { ["scope"] = scopeDefn, ["alias"] = modAlias, [" | table.insert(matchCriteriaMap[modLocalID], { | ||
["mod"] = modDefn, | |||
["scope"] = scopeDefn, | |||
["alias"] = modAlias, | |||
["props"] = propCriteria | |||
}) | |||
end | end | ||
Line 268: | Line 372: | ||
local modScopeDefn = p.convertScopeDataToDefinition(scopeData) | local modScopeDefn = p.convertScopeDataToDefinition(scopeData) | ||
for _, matchDefn in ipairs(modMatchCriteria) do | for _, matchDefn in ipairs(modMatchCriteria) do | ||
local scopeDefn, modAlias = matchDefn.scope, matchDefn.alias | local modDefn, scopeDefn, modAlias, propCriteria = matchDefn.mod, matchDefn.scope, matchDefn.alias, matchDefn.props | ||
local matchKey = 'unmatched' | local matchKey = 'unmatched' | ||
-- Check that: | -- Check that: | ||
Line 280: | Line 380: | ||
if ( | if ( | ||
p.doScopeDefinitionsMatch(modScopeDefn, scopeDefn) | p.doScopeDefinitionsMatch(modScopeDefn, scopeDefn) | ||
and p.checkScopeDataMeetsCriteria(scopeData, scopeDefn, modAlias) | and p.checkScopeDataMeetsCriteria(modDefn, scopeData, scopeDefn, modAlias) | ||
and p.checkScopeDataMeetsCriteria(scopeData, scopeDefn, | and p.checkScopeDataMeetsCriteria(modDefn, scopeData, scopeDefn, propCriteria) | ||
) then | ) then | ||
-- Add to matched table | -- Add to matched table | ||
Line 463: | Line 563: | ||
else | else | ||
local actionName = action.name | local actionName = action.name | ||
if actionName == nil and action | if actionName == nil then | ||
-- May use the product name instead | |||
local productKey = ( | |||
(scopeSourceID == 'melvorD:Firemaking' and 'logID') | |||
or 'productID' | |||
) | |||
local productID = action[productKey] | |||
if productID ~= nil then | |||
local productItem = GameData.getEntityByID('items', productID) | |||
actionName = productItem.name | |||
end | |||
end | end | ||
tVal = (action ~= nil and (actionName or v)) or Shared.printError('Unknown action: ' .. v, ' for scope source ID ' .. scopeSourceID) | tVal = (action ~= nil and (actionName or v)) or Shared.printError('Unknown action: ' .. v, ' for scope source ID ' .. scopeSourceID) | ||
Line 604: | Line 712: | ||
overflowText = table.concat({ | overflowText = table.concat({ | ||
'<br><span class="mw-collapsible mw-collapsed" data-expandtext=', | '<br><span class="mw-collapsible mw-collapsed" data-expandtext=', | ||
'"Show ' .. | '"Show ' .. Num.formatnum(modCount.overflow) .. ' more modifiers" ', | ||
'data-collapsetext="Hide">', | 'data-collapsetext="Hide">', | ||
table.concat(modArray.overflow, entrySep), | table.concat(modArray.overflow, entrySep), | ||
Line 655: | Line 763: | ||
function p.test3() | function p.test3() | ||
local item = GameData.getEntityByID('items', ' | local item = GameData.getEntityByID('items', 'melvorItA:Corrupted_Light_Consumable_I') | ||
local matchCriteria = { | local matchCriteria = { | ||
{ | { | ||
Line 663: | Line 771: | ||
--} | --} | ||
-- Should no longer match if changed to increased | -- Should no longer match if changed to increased | ||
["alias"] = ' | ["alias"] = 'increasedChanceNoDamageMining', | ||
}, | }, | ||
{ | { | ||
Line 683: | Line 791: | ||
["damageType"] = true | ["damageType"] = true | ||
} | } | ||
}, | |||
{ | |||
["mod"] = p.getModifierByID('flatBaseRandomProductQuantity'), | |||
["scope"] = { | |||
["item"] = true, | |||
["skill"] = true | |||
}, | |||
["props"] = p.convertCriteriaNamesToIDs({ ["item"] = 'Abyssal Stardust' }) | |||
} | } | ||
} | } | ||
return p.getMatchingModifiers(item.modifiers, matchCriteria | return p.getMatchingModifiers(item.modifiers, matchCriteria) | ||
end | end | ||
Line 699: | Line 815: | ||
return p.getModifierValue(matchedMods.matched) | return p.getModifierValue(matchedMods.matched) | ||
--return p.getMatchingModifiers(purch.contains.modifiers, matchCriteria) | --return p.getMatchingModifiers(purch.contains.modifiers, matchCriteria) | ||
end | |||
-- Checks p.getMatchCriteriaFromIDs() and p.getMatchingModifiers() | |||
-- Any entity processed by this should return a table with all modifiers | |||
-- matched and none unmatched. Unmatched modifiers implies an issue with one | |||
-- of the aforementioned functions | |||
function p.test5() | |||
local ent = GameData.getEntityByID('items', 'melvorF:Miners_Helmet') | |||
local entMods = ent.modifiers | |||
if entMods ~= nil then | |||
local modIDs = {} | |||
for modID, modDet in pairs(entMods) do | |||
table.insert(modIDs, { | |||
["id"] = modID, | |||
["type"] = 'id', | |||
["props"] = {} | |||
}) | |||
end | |||
local matchCriteria = p.getMatchCriteriaFromIDs(modIDs) | |||
mw.logObject(matchCriteria) | |||
mw.log('=======================================================') | |||
local matchingMods = p.getMatchingModifiers(entMods, matchCriteria) | |||
return matchingMods | |||
end | |||
return nil | |||
end | end | ||
return p | return p |
Latest revision as of 20:26, 4 September 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')
local Num = require('Module:Number')
-- 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,
['props'] = { ['valueType'] = 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',
['gameDataKey'] = 'damageTypes'
},
['realmID'] = {
['key'] = 'realm',
['templateKey'] = 'realmName',
['gameDataKey'] = 'realms'
},
['currencyID'] = {
['key'] = 'currency',
['templateKey'] = 'currencyName',
['gameDataKey'] = 'currencies'
},
['categoryID'] = {
['key'] = 'category',
['templateKey'] = 'categoryName'
},
['actionID'] = {
['key'] = 'action',
['templateKey'] = 'actionName'
},
['subcategoryID'] = {
['key'] = 'subcategory',
['templateKey'] = 'subcategoryName'
},
['itemID'] = {
['key'] = 'item',
['templateKey'] = 'itemName',
['gameDataKey'] = 'items'
},
['effectGroupID'] = {
['key'] = 'effectGroup',
['templateKey'] = 'effectGroupName',
['gameDataKey'] = 'combatEffectGroups'
},
}
local ScopeKeyToIDMap = {}
for idKey, defn in pairs(ScopeKeyMap) do
ScopeKeyToIDMap[defn.key] = idKey
end
-- 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
-- Given a table containing one or more sets of data criteria, combines those criteria into a
-- single set of criteria. Should the same criteria key be specified more than once, the value from
-- the last set containing that key will be retained
function p.combineDataCriteria(dataCriteriaList)
local rv = {}
for _, dataCriteria in ipairs(dataCriteriaList) do
for dataKey, dataValue in pairs(dataCriteria) do
rv[dataKey] = dataValue
end
end
return rv
end
-- Given data criteria where the values are entity names, converts those names to IDs.
function p.convertCriteriaNamesToIDs(dataCriteriaByName)
local dataCriteria = {}
for criteriaKey, criteriaName in pairs(dataCriteriaByName) do
if criteriaName == nil or criteriaName == '' then
error('Value for criteria ' .. criteriaKey .. ' cannot be nil', 2)
end
if criteriaKey == 'valueType' or criteriaKey == 'modType' then
-- Special cases:
-- valueType restricts by sign of modifier value
-- modType restricts by whether the modifier is deemed to be a positive or
-- negative to the target
dataCriteria[criteriaKey] = string.lower(criteriaName)
elseif ScopeKeyMap[criteriaKey] ~= nil then
-- ID specified directly
dataCriteria[criteriaKey] = criteriaName
else
local criteriaIDKey = ScopeKeyToIDMap[criteriaKey]
if criteriaIDKey == nil then
error('Invalid criteria specified: ' .. criteriaKey, 2)
end
local keyMap = ScopeKeyMap[criteriaIDKey]
local criteriaID = nil
if criteriaName == 'nil' then
-- Special value used to check for the absence of a particular
-- property by p.checkScopeDataMeetsCriteria() - passthrough
criteriaID = criteriaName
elseif keyMap.gameDataKey ~= nil then
local criteriaData = GameData.getEntityByName(keyMap.gameDataKey, criteriaName)
if criteriaData ~= nil then
criteriaID = criteriaData.id
end
elseif criteriaKey == 'skill' then
criteriaID = Common.getSkillID(criteriaName)
else
error('Criteria ' .. criteriaKey .. ' is currently unsupported', 2)
end
if criteriaID == nil then
error('Unknown ' .. criteriaKey .. ': ' .. criteriaName, 2)
end
dataCriteria[criteriaIDKey] = criteriaID
end
end
return dataCriteria
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.
-- Passing the string 'nil' as a value for any criteria indicates that a match should only
-- occur if the given property is absent from the given scope data
-- dataCriteria accepts two additional properties that aren't usually found in scope data, being:
-- - valueType: Indicates whether the value of the modifier should be positive ('pos') or
-- negative ('neg') in order for the criteria to be met
-- - modType: Similar to valueType, except determines whether the modifier is a positive ('pos')
-- or negative ('neg') to the player, rather than simply determining the sign of
-- the value as valueType does
function p.checkScopeDataMeetsCriteria(modDefn, 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 criteriaKey == 'modType' then
if criteriaValue ~= nil and scopeData.value ~= nil then
local isInverted = modDefn.inverted
if isInverted == nil then
isInverted = false
end
local isPositive = ((isInverted and scopeData.value < 0) or (not isInverted and scopeData.value > 0))
if not (
(criteriaValue == 'pos' and isPositive)
or (criteriaValue == 'neg' and not isPositive)
) then
-- Mod type criteria not met
return false
end
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] == '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)
local matchCriteria = {}
-- For modifier IDs, find the relevant mod definition and add all allowed scopes
if modifierIDs ~= nil then
for _, idObject in ipairs(modifierIDs) do
local modifierID = idObject.id
local modType = idObject.type
local modProps = idObject.props or {}
if modType == 'id' then
local modDefn = p.getModifierByID(modifierID)
if modDefn == nil then
error('No such modifier ID: ' .. modifierID, 2)
end
-- Add all scopes
for _, allowedScope in ipairs(modDefn.allowedScopes) do
table.insert(matchCriteria, {
["mod"] = modDefn,
["scope"] = allowedScope.scopes or {},
["props"] = modProps
})
end
elseif modType == 'alias' then
-- For alias IDs, simply add these one at a time to the table
table.insert(matchCriteria, {
["alias"] = modifierID,
["props"] = modProps
})
else
error('Unknown modifier ID type: ' .. (modType or 'nil'), 2)
end
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,
-- ["props"] = modProps
-- }
-- 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.
-- props is an optional property, if specified then a modifier is only matched
-- if the properties (skillID, itemID, etc.) also match.
function p.getMatchingModifiers(modifiers, matchCriteria)
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, propCriteria = matchDefn.mod, matchDefn.scope, {}, (matchDefn.props or {})
if matchDefn.alias ~= nil then
local aliasData = p.getModifierByAlias(matchDefn.alias)
if aliasData == nil then
error('No such modifier alias: ' .. matchDefn.alias, 2)
else
local aliasProps = p.combineDataCriteria({propCriteria, aliasData.props})
modDefn, scopeDefn, modAlias, propCriteria = aliasData.mod, aliasData.scope.scopes, aliasData.alias, aliasProps
end
end
local modNS, modLocalID = Shared.getLocalID(modDefn.id)
if matchCriteriaMap[modLocalID] == nil then
matchCriteriaMap[modLocalID] = {}
end
table.insert(matchCriteriaMap[modLocalID], {
["mod"] = modDefn,
["scope"] = scopeDefn,
["alias"] = modAlias,
["props"] = propCriteria
})
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 modDefn, scopeDefn, modAlias, propCriteria = matchDefn.mod, matchDefn.scope, matchDefn.alias, matchDefn.props
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(modDefn, scopeData, scopeDefn, modAlias)
and p.checkScopeDataMeetsCriteria(modDefn, scopeData, scopeDefn, propCriteria)
) 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 == 'melvorD:AttackSpell' then
-- Uses spell categories, contained within magic skill data
scopeSourceData = GameData.skillData.Magic
elseif scopeSourceID == 'melvorD:CombatArea' then
scopeSourceData = GameData.rawData
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('Scope data is required for scope type ' .. tKey, 2)
end
local catKey = (
(scopeSourceID == 'melvorD:AttackSpell' and 'spellCategories')
or (scopeSourceID == 'melvorD:CombatArea' and 'combatAreaCategories')
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 then
-- May use the product name instead
local productKey = (
(scopeSourceID == 'melvorD:Firemaking' and 'logID')
or 'productID'
)
local productID = action[productKey]
if productID ~= nil then
local productItem = GameData.getEntityByID('items', productID)
actionName = productItem.name
end
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 == 'melvorD: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 ' .. Num.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 obj = GameData.getEntityByID('pets', 'melvorF:Harley') --'melvorD:Aorpheats_Signet_Ring')
return p.getModifiersText(obj.modifiers, true, false, 5)
--return p.getModifierSkills(item.modifiers)
end
function p.test3()
local item = GameData.getEntityByID('items', 'melvorItA:Corrupted_Light_Consumable_I')
local matchCriteria = {
{
--["mod"] = p.getModifierByID('skillItemDoublingChance'),
--["scope"] = {
-- ["skill"] = true
--}
-- Should no longer match if changed to increased
["alias"] = 'increasedChanceNoDamageMining',
},
{
["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
}
},
{
["mod"] = p.getModifierByID('flatBaseRandomProductQuantity'),
["scope"] = {
["item"] = true,
["skill"] = true
},
["props"] = p.convertCriteriaNamesToIDs({ ["item"] = 'Abyssal Stardust' })
}
}
return p.getMatchingModifiers(item.modifiers, matchCriteria)
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
-- Checks p.getMatchCriteriaFromIDs() and p.getMatchingModifiers()
-- Any entity processed by this should return a table with all modifiers
-- matched and none unmatched. Unmatched modifiers implies an issue with one
-- of the aforementioned functions
function p.test5()
local ent = GameData.getEntityByID('items', 'melvorF:Miners_Helmet')
local entMods = ent.modifiers
if entMods ~= nil then
local modIDs = {}
for modID, modDet in pairs(entMods) do
table.insert(modIDs, {
["id"] = modID,
["type"] = 'id',
["props"] = {}
})
end
local matchCriteria = p.getMatchCriteriaFromIDs(modIDs)
mw.logObject(matchCriteria)
mw.log('=======================================================')
local matchingMods = p.getMatchingModifiers(entMods, matchCriteria)
return matchingMods
end
return nil
end
return p