Module:GameData/doc: Difference between revisions
From Melvor Idle
(Update documentation in line with changes to how data is loaded) |
(Support all remaining data modifications & dependent data) |
||
(10 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
The '''GameData''' module is the source of all game data which many other Lua modules rely upon. This module deals with the initial loading of the game data structure, and then enables other modules to access this both via a library of functions (preferred) and in its raw format. | The '''GameData''' module is the source of all game data which many other Lua modules rely upon. This module deals with the initial loading of the game data structure, and then enables other modules to access this both via a library of functions (preferred) and in its raw format. | ||
The game data used by the wiki is currently at version <b>{{#invoke:GameData|getDataVersionText}}</b>. | |||
To generate game data, do the following: | To generate game data, do the following: | ||
Line 7: | Line 9: | ||
# Open the browser console/developer mode (usually by hitting the F12 key for most browsers) | # Open the browser console/developer mode (usually by hitting the F12 key for most browsers) | ||
# Within the browser console, enter the following code then hit enter. If successful, the game data should appear within the console | # Within the browser console, enter the following code then hit enter. If successful, the game data should appear within the console | ||
# Copy the game data & update [[Module:GameData/data | # Copy the game data & update [[Module:GameData/data]], [[Module:GameData/data2]], [[Module:GameData/data3]] accordingly | ||
{{SpoilerBox|color=default|title=Code|text=<syntaxhighlight lang="javascript" line>class Wiki { | {{SpoilerBox|color=default|title=Code|text=<syntaxhighlight lang="javascript" line>class Wiki { | ||
Line 13: | Line 15: | ||
this.debugMode = false; | this.debugMode = false; | ||
this.prettyPrint = false; | this.prettyPrint = false; | ||
this. | this.customLocalizations = { | ||
// Contains custom localization strings, to override any game provided localizations. | |||
// To be used sparingly, for instances where 2+ objects of the same type | |||
// (e.g. monsters) have the same name, as this isn't convenient to deal with in Lua | |||
// TotH curse also named 'Madness' | |||
MAGIC_ABYSSAL_NAME_Madness: 'Madness (ItA)', | |||
// Stronghold boss monsters, where names overlap with normal monster variants | |||
MONSTER_NAME_FierceDevilBoss: 'Fierce Devil (Stronghold)', | |||
MONSTER_NAME_ElementalistBoss: 'Elementalist (Stronghold)', | |||
MONSTER_NAME_PratTheGuardianOfSecretsBoss: 'Prat, the Guardian of Secrets (Stronghold)', | |||
MONSTER_NAME_MysteriousFigurePhase1Stronghold: 'Mysterious Figure - Phase 1 (Stronghold)', | |||
MONSTER_NAME_MysteriousFigurePhase2Stronghold: 'Mysterious Figure - Phase 2 (Stronghold)', | |||
MONSTER_NAME_AhreniaStronghold: 'Ahrenia (Stronghold)' | |||
}; | |||
this.namespaces = { | this.namespaces = { | ||
melvorD: { displayName: | melvorD: { | ||
melvorF: { displayName: | displayName: 'Demo', | ||
melvorTotH: { displayName: | packFile: 'melvorDemo.json', | ||
melvorAoD: { displayName: | }, | ||
melvorF: { | |||
displayName: 'Full Version', | |||
packFile: 'melvorFull.json', | |||
}, | |||
melvorTotH: { | |||
displayName: 'Throne of the Herald', | |||
packFile: 'melvorTotH.json', | |||
}, | |||
melvorAoD: { | |||
displayName: 'Atlas of Discovery', | |||
packFile: 'melvorExpansion2.json', | |||
}, | |||
melvorBirthday2023: { | |||
displayName: 'Melvor Birthday 2023', | |||
packFile: 'melvorBirthday2023.json', | |||
}, | |||
melvorItA: { | |||
displayName: 'Into the Abyss', | |||
packFile: 'melvorItA.json', | |||
}, | |||
}; | }; | ||
this.registeredNamespaces = []; | |||
// List of categories to be excluded from the generated game data. | |||
// These serve no purpose for the wiki and so would otherwise bloat the data | |||
this.excludedCategories = [ | |||
'pages', | |||
'steamAchievements', | |||
'tutorialStageOrder', | |||
'tutorialStages' | |||
]; | |||
// Check all required namespaces are registered, as there are still some bits of data extracted from in-game rather than the data packages | // Check all required namespaces are registered, as there are still some bits of data extracted from in-game rather than the data packages | ||
Object.keys(this.namespaces).forEach((nsID) => { | Object.keys(this.namespaces).forEach((nsID) => { | ||
const nsTest = game.registeredNamespaces.getNamespace(nsID); | const nsTest = game.registeredNamespaces.getNamespace(nsID); | ||
if (nsTest === undefined) { | if (nsTest === undefined) { | ||
throw new Error(`Namespace ${ nsID } (${ this.namespaces[nsID].displayName }) is not registered - Ensure you are signed in and have the expansion.`); | throw new Error( | ||
`Namespace ${nsID} (${this.namespaces[nsID].displayName}) is not registered - Ensure you are signed in and have the expansion.` | |||
); | |||
} | } | ||
}); | }); | ||
Line 31: | Line 79: | ||
// pages (Module:GameData then combines the data into a single structure upon | // pages (Module:GameData then combines the data into a single structure upon | ||
// initialization). | // initialization). | ||
this.maxPageBytes = 2*1024**2; // 2048KB | this.maxPageBytes = 2 * 1024 ** 2; // 2048KB | ||
this.printPages = [ | this.printPages = [ | ||
{ includeCategories: '*', destination: 'Module:GameData/data' }, | { includeCategories: '*', destination: 'Module:GameData/data' }, | ||
{ includeCategories: ['items', 'itemUpgrades', 'itemSynergies', ' | { | ||
includeCategories: ['items'], | |||
destination: 'Module:GameData/data2', | |||
}, | |||
{ | |||
includeCategories: [ | |||
'itemUpgrades', | |||
'itemSynergies', | |||
'modifiers', | |||
'shopPurchases', | |||
'realms', | |||
'damageTypes', | |||
'combatTriangleSets', | |||
'randomAbyssalGems', | |||
'randomFragments', | |||
'randomFiremakingOils', | |||
'ancientRelics', | |||
'attackSpells', | |||
'attacks', | |||
'combatPassives', | |||
'monsters', | |||
'bankSortOrder', | |||
'combatEffects', | |||
'combatEffectTemplates', | |||
'combatEffectGroups' | |||
], | |||
destination: 'Module:GameData/data3', | |||
}, | |||
]; | ]; | ||
Line 40: | Line 115: | ||
this.gameData = {}; | this.gameData = {}; | ||
this.skillDataInit = {}; | this.skillDataInit = {}; | ||
}; | } | ||
getDataPackURL(nsID) { | |||
return 'https://' + location.hostname + '/assets/data/' + this.namespaces[nsID].packFile + '?' + DATA_VERSION.toString(); | |||
} | |||
async getWikiData() { | async getWikiData() { | ||
if (!isLoaded) { | if (!isLoaded) { | ||
Line 47: | Line 125: | ||
for (const nsIdx in Object.keys(this.namespaces)) { | for (const nsIdx in Object.keys(this.namespaces)) { | ||
const ns = Object.keys(this.namespaces)[nsIdx]; | const ns = Object.keys(this.namespaces)[nsIdx]; | ||
const dataURL = this. | const dataURL = this.getDataPackURL(ns); | ||
console.log(`URL: ${ dataURL }`); | console.log(`URL: ${dataURL}`); | ||
const dataPackage = await this.getDataPackage(dataURL); | const dataPackage = await this.getDataPackage(dataURL); | ||
if (dataPackage.namespace === undefined) { | if (dataPackage.namespace === undefined) { | ||
throw new Error(`Data package has no namespace: ${ dataURL }`); | throw new Error(`Data package has no namespace: ${dataURL}`); | ||
} else if (dataPackage.data === undefined) { | |||
throw new Error(`Data package has no data: ${dataURL}`); | |||
} | } | ||
console.log( | |||
`Obtained data for namespace ${dataPackage.namespace}, ${JSON.stringify( | |||
dataPackage.data | |||
console.log(`Obtained data for namespace ${ dataPackage.namespace }, ${ JSON.stringify(dataPackage.data).length.toLocaleString() } bytes`); | ).length.toLocaleString()} bytes` | ||
); | |||
this.processDataPackage(dataPackage); | this.processDataPackage(dataPackage); | ||
console.log(`After transformation: ${ JSON.stringify(dataPackage.data).length.toLocaleString() } bytes`); | console.log(`After transformation: ${JSON.stringify(dataPackage.data).length.toLocaleString()} bytes`); | ||
} | } | ||
// Process dependent data after all packages processed | |||
console.log('Processing dependent data for all packages...'); | |||
this.processDependentData(); | |||
// All data packages should now be within this.gameData | // All data packages should now be within this.gameData | ||
} | } | ||
getGameFileVersion() { | |||
const fileDOM = document.querySelector('#sidebar ul.nav-main'); | const fileDOM = document.querySelector('#sidebar ul.nav-main'); | ||
let fileVer = | let fileVer = 'Unknown'; | ||
if (fileDOM !== null && fileDOM.dataset !== undefined) { | if (fileDOM !== null && fileDOM.dataset !== undefined) { | ||
return fileDOM.dataset.fileVersion; | |||
} | } | ||
return gameVersion + ' (' + | } | ||
getGameVersion() { | |||
return gameVersion; | |||
} | |||
getGameVersionText() { | |||
return this.getGameVersion() + ' (' + this.getGameFileVersion() + ')'; | |||
} | } | ||
getObjectByID(data, objectID, idKey = 'id') { | getObjectByID(data, objectID, idKey = 'id') { | ||
if | if (data !== undefined && objectID !== undefined) { | ||
return data.find((obj) => obj[idKey] === objectID); | return data.find((obj) => obj[idKey] === objectID); | ||
} | } | ||
Line 78: | Line 167: | ||
if (Array.isArray(page.includeCategories)) { | if (Array.isArray(page.includeCategories)) { | ||
return page.includeCategories; | return page.includeCategories; | ||
} | } else if (page.includeCategories === '*') { | ||
// Special value, include all categories other than those included within | // Special value, include all categories other than those included within | ||
// other pages | // other pages | ||
return Object.keys(this.gameData).filter((cat) => !this.printPages.some((p) => Array.isArray(p.includeCategories) && p.includeCategories.includes(cat))); | return Object.keys(this.gameData).filter( | ||
(cat) => !this.printPages.some((p) => Array.isArray(p.includeCategories) && p.includeCategories.includes(cat)) | |||
); | |||
} | } | ||
} | } | ||
escapeQuotes(data) { | escapeQuotes(data) { | ||
var newData = data.replace(/\\/g, '\\\\') | var newData = data.replace(/\\/g, '\\\\'); | ||
newData = newData.replace(/'/g, "\\'"); | newData = newData.replace(/'/g, "\\'"); | ||
newData = newData.replace(/"/g, '\\"'); | newData = newData.replace(/"/g, '\\"'); | ||
Line 93: | Line 183: | ||
formatJSONData(category, data) { | formatJSONData(category, data) { | ||
if (data === undefined) { | if (data === undefined) { | ||
console.warn(`dataFormatter: Data for category ${ category } is undefined`); | console.warn(`dataFormatter: Data for category ${category} is undefined`); | ||
return ''; | return ''; | ||
} | } | ||
Line 101: | Line 191: | ||
if (category === 'skillData') { | if (category === 'skillData') { | ||
return '"' + category + '":[' + data.map((x) => this.escapeQuotes(JSON.stringify(x))).join(",' ..\n'") + ']'; | return '"' + category + '":[' + data.map((x) => this.escapeQuotes(JSON.stringify(x))).join(",' ..\n'") + ']'; | ||
} | } else { | ||
return '"' + category + '":' + this.escapeQuotes(JSON.stringify(data)); | return '"' + category + '":' + this.escapeQuotes(JSON.stringify(data)); | ||
} | } | ||
Line 117: | Line 206: | ||
const inclCat = this.getCategoriesForPage(page); | const inclCat = this.getCategoriesForPage(page); | ||
inclCat.forEach((cat) => { | inclCat.forEach((cat) => { | ||
dataLengths.push | dataLengths.push({ | ||
page: page.destination, | page: page.destination, | ||
category: cat, | category: cat, | ||
length: this.formatJSONData(cat, this.gameData[cat]).length | length: this.formatJSONData(cat, this.gameData[cat]).length, | ||
} | }); | ||
}); | }); | ||
}); | }); | ||
Line 134: | Line 223: | ||
const result = await this.getWikiData(); | const result = await this.getWikiData(); | ||
} | } | ||
console.log('Printing data for game version ' + this. | console.log('Printing data for game version ' + this.getGameVersionText()); | ||
this.printPages.forEach((page) => { | this.printPages.forEach((page) => { | ||
const inclCat = this.getCategoriesForPage(page); | const inclCat = this.getCategoriesForPage(page); | ||
let gameDataFiltered = {}; | let gameDataFiltered = {}; | ||
inclCat.forEach((cat) => gameDataFiltered[cat] = this.gameData[cat]); | inclCat.forEach((cat) => (gameDataFiltered[cat] = this.gameData[cat])); | ||
// Convert game data into a JSON string for export | // Convert game data into a JSON string for export | ||
Line 144: | Line 233: | ||
if (this.prettyPrint) { | if (this.prettyPrint) { | ||
dataText = JSON.stringify(gameDataFiltered, undefined, '\t'); | dataText = JSON.stringify(gameDataFiltered, undefined, '\t'); | ||
} | } else { | ||
dataText = JSON.stringify(gameDataFiltered); | dataText = JSON.stringify(gameDataFiltered); | ||
} | } | ||
console.log(`For page "${ page.destination }" (${ dataText.length.toLocaleString() } bytes):`); | console.log(`For page "${page.destination}" (${dataText.length.toLocaleString()} bytes):`); | ||
if (dataText.length > this.maxPageBytes) { | if (dataText.length > this.maxPageBytes) { | ||
console.warn(`Page "${ page.destination }" exceeds max page size of ${ (this.maxPageBytes / 1024).toLocaleString() }KB by ${ (dataText.length - this.maxPageBytes).toLocaleString() } bytes. Consider amending the printPages configuration to move some data categories from this page onto other pages.`) | console.warn( | ||
`Page "${page.destination}" exceeds max page size of ${(this.maxPageBytes / 1024).toLocaleString()}KB by ${( | |||
dataText.length - this.maxPageBytes | |||
).toLocaleString()} bytes. Consider amending the printPages configuration to move some data categories from this page onto other pages.` | |||
); | |||
} | } | ||
console.log(dataText); | console.log(dataText); | ||
}); | |||
} | } | ||
async getDataPackage(url) { | async getDataPackage(url) { | ||
Line 162: | Line 254: | ||
return await fetch(url, { | return await fetch(url, { | ||
method: 'GET', | method: 'GET', | ||
headers | headers, | ||
}).then(function(response) { | }).then(function (response) { | ||
if (!response.ok) { | if (!response.ok) { | ||
throw new Error(`Couldn't fetch data package from URL: ${ url }`); | throw new Error(`Couldn't fetch data package from URL: ${url}`); | ||
} | } | ||
return response.json(); | return response.json(); | ||
Line 185: | Line 277: | ||
const packData = dataPackage.data; | const packData = dataPackage.data; | ||
Object.keys(packData). | Object.keys(packData) | ||
.filter((categoryName) => !this.excludedCategories.includes(categoryName)) | |||
.forEach((categoryName) => { | |||
this.transformDataNode(ns, categoryName, packData, categoryName); | |||
}); | |||
} | } | ||
transformDataNode(ns, categoryName, parentNode, nodeKey) { | transformDataNode(ns, categoryName, parentNode, nodeKey) { | ||
Line 212: | Line 294: | ||
// Recursive call to ensure all data is transformed, regardless of its depth | // Recursive call to ensure all data is transformed, regardless of its depth | ||
dataNode.forEach((entity, idx) => this.transformDataNode(ns, categoryName, dataNode, idx)); | dataNode.forEach((entity, idx) => this.transformDataNode(ns, categoryName, dataNode, idx)); | ||
} | } else if (typeof dataNode === 'object' && dataNode !== null) { | ||
// Iterate properties of object, checking if each should be deleted or transformed | // Iterate properties of object, checking if each should be deleted or transformed | ||
Object.keys(dataNode).forEach((key) => { | Object.keys(dataNode).forEach((key) => { | ||
Line 219: | Line 300: | ||
if (this.isPropertyFiltered(categoryName, dataNode, key)) { | if (this.isPropertyFiltered(categoryName, dataNode, key)) { | ||
delete dataNode[key]; | delete dataNode[key]; | ||
} | } else if (typeof dataNode[key] === 'object' && dataNode[key] !== null) { | ||
// If an object (either an array or key/value store) is within the current | // If an object (either an array or key/value store) is within the current | ||
// object then we must traverse this too | // object then we must traverse this too | ||
this.transformDataNode(ns, categoryName, dataNode, key); | this.transformDataNode(ns, categoryName, dataNode, key); | ||
} | } else { | ||
// Transform property, if a transformation is defined below | // Transform property, if a transformation is defined below | ||
switch(key) { | switch (key) { | ||
case 'id': | case 'id': | ||
// Add namespace to ID if it isn't already | // Add namespace to ID if it isn't already | ||
dataNode[key] = this.getNamespacedID(ns, dataNode[key]); | const id = dataNode[key]; | ||
if (!Number.isInteger(id)) dataNode[key] = this.getNamespacedID(ns, dataNode[key]); | |||
break; | break; | ||
} | } | ||
Line 243: | Line 323: | ||
// Special case for skillData so that certain values initialized when the various Skill | // Special case for skillData so that certain values initialized when the various Skill | ||
// classes are initialized may be added here also | // classes are initialized may be added here also | ||
if | if (categoryName === 'skillData' && dataNode.skillID !== undefined && dataNode.data !== undefined) { | ||
// We are currently at the topmost level of a skill object | // We are currently at the topmost level of a skill object | ||
const gameSkill = game.skills.getObjectByID(dataNode.skillID); | const gameSkill = game.skills.getObjectByID(dataNode.skillID); | ||
// For every skill with mastery, add mastery checkpoint descriptions | // For every skill with mastery, add mastery checkpoint descriptions | ||
if (gameSkill instanceof SkillWithMastery && dataNode.data.masteryTokenID !== undefined && dataNode.data.masteryCheckpoints === undefined) { | if ( | ||
gameSkill instanceof SkillWithMastery && | |||
dataNode.data.masteryTokenID !== undefined && | |||
dataNode.data.masteryCheckpoints === undefined | |||
) { | |||
const localID = this.getLocalID(dataNode.skillID); | const localID = this.getLocalID(dataNode.skillID); | ||
dataNode.data.baseMasteryPoolCap = gameSkill.baseMasteryPoolCap; | dataNode.data.baseMasteryPoolCap = gameSkill.baseMasteryPoolCap; | ||
dataNode.data.masteryCheckpoints = []; | dataNode.data.masteryCheckpoints = []; | ||
masteryCheckpoints.forEach((pct, idx) => { | masteryCheckpoints.forEach((pct, idx) => { | ||
dataNode.data.masteryCheckpoints[idx] = this.getLangString('MASTERY_CHECKPOINT', `${ localID }_${ idx }`); | dataNode.data.masteryCheckpoints[idx] = this.getLangString('MASTERY_CHECKPOINT', `${localID}_${idx}`); | ||
}); | }); | ||
} | } | ||
Line 259: | Line 343: | ||
// Import other attributes varying by skill | // Import other attributes varying by skill | ||
let importKeys = []; | let importKeys = []; | ||
switch(dataNode.skillID) { | switch (dataNode.skillID) { | ||
case 'melvorD: | case 'melvorD:Mining': | ||
importKeys = [ | importKeys = ['baseInterval', 'baseRockHP', 'passiveRegenInterval']; | ||
dataNode.data.baseGemChance = 1; | |||
dataNode.data.rockTypes = loadedLangJson.MINING_TYPE; | |||
break; | break; | ||
case ' | case 'melvorItA:Harvesting': | ||
importKeys = [ | importKeys = [ | ||
'baseInterval', | 'baseInterval', | ||
' | 'baseVeinIntensity', | ||
'passiveRegenInterval' | 'passiveRegenInterval', | ||
'uniqueProductChance', | |||
'hpCheckpoints', | |||
]; | ]; | ||
break; | break; | ||
case 'melvorD:Smithing': | case 'melvorD:Smithing': | ||
Line 281: | Line 363: | ||
case 'melvorD:Runecrafting': | case 'melvorD:Runecrafting': | ||
case 'melvorD:Herblore': | case 'melvorD:Herblore': | ||
importKeys = [ | importKeys = ['baseInterval']; | ||
break; | break; | ||
case 'melvorD:Thieving': | case 'melvorD:Thieving': | ||
importKeys = [ | importKeys = ['baseInterval', 'baseStunInterval', 'itemChance', 'baseAreaUniqueChance']; | ||
break; | break; | ||
case 'melvorD:Summoning': | case 'melvorD:Summoning': | ||
importKeys = [ | importKeys = ['baseInterval']; | ||
const sumKeys = ['recipeAPCost', 'recipeGPCost', 'markLevels']; | |||
sumKeys.forEach((k) => (dataNode.data[k] = Summoning[k])); | |||
const sumKeys = [ | |||
sumKeys.forEach((k) => dataNode.data[k] = Summoning[k]); | |||
break; | break; | ||
case 'melvorD:Astrology': | case 'melvorD:Astrology': | ||
Line 313: | Line 378: | ||
'standardModifierLevels', | 'standardModifierLevels', | ||
'uniqueModifierLevels', | 'uniqueModifierLevels', | ||
' | 'abyssalModifierLevels', | ||
' | 'baseInterval', | ||
]; | ]; | ||
astKeys.forEach((k) => dataNode.data[k] = Astrology[k]); | astKeys.forEach((k) => (dataNode.data[k] = Astrology[k])); | ||
break; | break; | ||
case 'melvorD:Township': | case 'melvorD:Township': | ||
// Remap a number of keys from their in-game names | // Remap a number of keys from their in-game names | ||
const townKeys = [ | const townKeys = [ | ||
{from: 'BASE_STORAGE', to: 'baseStorage'}, | { from: 'BASE_STORAGE', to: 'baseStorage' }, | ||
{from: 'BASE_TAX_RATE', to: 'baseTaxRate'}, | { from: 'BASE_TAX_RATE', to: 'baseTaxRate' }, | ||
{from: 'DECREASED_BUILDING_COST_CAP', to: 'decreasedBuildingCostCap' }, | { from: 'DECREASED_BUILDING_COST_CAP', to: 'decreasedBuildingCostCap' }, | ||
{from: 'GP_PER_CITIZEN', to: 'gpPerCitizen'}, | { from: 'GP_PER_CITIZEN', to: 'gpPerCitizen' }, | ||
{from: 'MAX_WORSHIP', to: 'maxWorship'}, | { from: 'MAX_WORSHIP', to: 'maxWorship' }, | ||
{from: 'MINIMUM_HEALTH', to: 'minimumHealth'}, | { from: 'MINIMUM_HEALTH', to: 'minimumHealth' }, | ||
{from: 'populationForTier', to: 'populationForTier'}, | { from: 'populationForTier', to: 'populationForTier' }, | ||
{from: 'TICK_LENGTH', to: 'tickLength'}, | { from: 'TICK_LENGTH', to: 'tickLength' }, | ||
{from: 'RARE_SEASON_CHANCE', to: 'rareSeasonChance'}, | { from: 'RARE_SEASON_CHANCE', to: 'rareSeasonChance' }, | ||
{from: 'WORSHIP_CHANGE_COST', to: 'worshipChangeCost'}, | { from: 'WORSHIP_CHANGE_COST', to: 'worshipChangeCost' }, | ||
{from: 'WORSHIP_CHECKPOINTS', to: 'worshipCheckpoints'}, | { from: 'WORSHIP_CHECKPOINTS', to: 'worshipCheckpoints' }, | ||
{ from: 'BASE_MAX_HEALTH', to: 'baseMaxHealth' }, | |||
{ from: 'abyssalTierRequirements', to: 'abyssalTierRequirements' }, | |||
{ from: 'BASE_SOUL_STORAGE', to: 'baseSoulStorage' }, | |||
]; | ]; | ||
townKeys.forEach((k) => dataNode.data[k.to] = gameSkill[k.from]); | townKeys.forEach((k) => (dataNode.data[k.to] = gameSkill[k.from])); | ||
// Add task categories & localization of name | // Add task categories & localization of name | ||
const taskCategories = Array.from(new Set(gameSkill.tasks.tasks.allObjects.map((t) => t.category))); | const taskCategories = Array.from(new Set(gameSkill.tasks.tasks.allObjects.map((t) => t.category))); | ||
dataNode.data.taskCategories = taskCategories.map(( | dataNode.data.taskCategories = taskCategories.map((category) => ({ | ||
id: category.id, | |||
name: category.name, | |||
})); | |||
break; | break; | ||
} | } | ||
if (importKeys.length > 0) { | if (importKeys.length > 0) { | ||
importKeys.forEach((k) => dataNode.data[k] = gameSkill[k]); | importKeys.forEach((k) => (dataNode.data[k] = gameSkill[k])); | ||
} | } | ||
} | } | ||
Line 352: | Line 420: | ||
} | } | ||
} | } | ||
registerPackData(packData) { | |||
Object.keys(packData) | |||
.filter((categoryName) => !this.excludedCategories.includes(categoryName)) | |||
.forEach((categoryName) => { | |||
let categoryData = packData[categoryName]; | let categoryData = packData[categoryName]; | ||
// Some data is adjusted before combining - do this here | // Some data is adjusted before combining - do this here | ||
if (['combatAreas', 'dungeons', 'slayerAreas'].includes(categoryName)) { | if (['combatAreas', 'dungeons', 'slayerAreas', 'abyssDepths', 'strongholds'].includes(categoryName)) { | ||
// Add area type to each area object | // Add area type to each area object | ||
const areaTypes = { | const areaTypes = { | ||
combatAreas: 'combatArea', | |||
' | dungeons: 'dungeon', | ||
' | slayerAreas: 'slayerArea', | ||
} | strongholds: 'stronghold', | ||
abyssDepths: 'abyssDepth', | |||
}; | |||
const areaType = areaTypes[categoryName]; | const areaType = areaTypes[categoryName]; | ||
const newData = structuredClone(categoryData); | const newData = structuredClone(categoryData); | ||
newData.forEach((x) => x.type = areaType) | newData.forEach((x) => (x.type = areaType)); | ||
categoryData = newData; | categoryData = newData; | ||
} | } | ||
else if (categoryName === 'golbinRaid') { | else if (categoryName === 'golbinRaid') { | ||
} | } | ||
// Data must be pushed into the consoldiated data, rules for vary | // Data must be pushed into the consoldiated data, rules for vary | ||
// depending on the category in question | // depending on the category in question | ||
switch(categoryName) { | switch (categoryName) { | ||
case 'realms': | |||
case 'attackSpellbooks': | |||
case 'damageTypes': | |||
case 'equipmentSlots': | |||
case 'combatAreaCategories': | |||
case 'combatEffects': | |||
case 'combatEffectGroups': | |||
case 'combatEffectTables': | |||
case 'combatEffectTemplates': | |||
case 'combatTriangleSets': | |||
case 'masterPoolBonuses': | |||
case 'masteryLevelUnlocks': | |||
case 'masteryLevelBonuses': | |||
case 'masterPoolBonuses': | |||
case 'ancientRelics': | case 'ancientRelics': | ||
case ' | case 'attackSpells': | ||
case 'attackStyles': | case 'attackStyles': | ||
case 'attacks': | case 'attacks': | ||
Line 402: | Line 469: | ||
case 'curseSpells': | case 'curseSpells': | ||
case 'dungeons': | case 'dungeons': | ||
case 'strongholds': | |||
case 'abyssDepths': | |||
case 'gamemodes': | case 'gamemodes': | ||
case 'itemEffects': | case 'itemEffects': | ||
Line 409: | Line 478: | ||
case 'items': | case 'items': | ||
case 'lore': | case 'lore': | ||
case 'modifiers': | |||
case 'monsters': | case 'monsters': | ||
case 'pages': | case 'pages': | ||
Line 415: | Line 485: | ||
case 'randomGems': | case 'randomGems': | ||
case 'randomSuperiorGems': | case 'randomSuperiorGems': | ||
case 'randomAbyssalGems': | |||
case 'randomFragments': | |||
case 'randomFiremakingOils': | |||
case 'shopCategories': | case 'shopCategories': | ||
case 'shopPurchases': | case 'shopPurchases': | ||
case 'shopUpgradeChains': | case 'shopUpgradeChains': | ||
case 'skillLevelCapIncreases': | |||
case 'slayerAreas': | case 'slayerAreas': | ||
case ' | case 'slayerTaskCategories': | ||
case 'steamAchievements': | case 'steamAchievements': | ||
case 'tutorialStages': | case 'tutorialStages': | ||
Line 428: | Line 501: | ||
// Category doesn't exist yet in consolidated data, so create it | // Category doesn't exist yet in consolidated data, so create it | ||
this.gameData[categoryName] = categoryData; | this.gameData[categoryName] = categoryData; | ||
} | } else { | ||
this.gameData[categoryName].push(...categoryData); | this.gameData[categoryName].push(...categoryData); | ||
} | } | ||
break; | break; | ||
case 'ancientRelicsDisplayOrder': | |||
case 'bankSortOrder': | case 'bankSortOrder': | ||
case 'combatAreaCategoryOrder': | |||
case 'combatAreaDisplayOrder': | case 'combatAreaDisplayOrder': | ||
case 'dungeonDisplayOrder': | case 'dungeonDisplayOrder': | ||
case 'shopCategoryOrder': | case 'shopCategoryOrder': | ||
case 'shopDisplayOrder': | case 'shopDisplayOrder': | ||
case 'skillTreesDisplayOrder': | |||
case 'slayerAreaDisplayOrder': | case 'slayerAreaDisplayOrder': | ||
case 'tutorialStageOrder': | case 'tutorialStageOrder': | ||
Line 449: | Line 524: | ||
this.gameData[categoryName] = categoryData; | this.gameData[categoryName] = categoryData; | ||
this.gameData.golbinRaid.possibleModifiers = RaidManager.possibleModifiers; | this.gameData.golbinRaid.possibleModifiers = RaidManager.possibleModifiers; | ||
} | } else { | ||
Object.keys(categoryData).forEach((dataKey) => { | Object.keys(categoryData).forEach((dataKey) => { | ||
if ( | if ( | ||
this.gameData[categoryName][dataKey] === undefined || | |||
!Array.isArray(this.gameData[categoryName][dataKey]) | |||
) { | |||
// Property is undefined or isn't an array | // Property is undefined or isn't an array | ||
this.gameData[categoryName][dataKey] = categoryData[dataKey]; | this.gameData[categoryName][dataKey] = categoryData[dataKey]; | ||
} | } else { | ||
// Property is an array | // Property is an array | ||
this.gameData[categoryName][dataKey].push(...categoryData[dataKey]); | this.gameData[categoryName][dataKey].push(...categoryData[dataKey]); | ||
Line 481: | Line 557: | ||
Object.keys(skillData.data).forEach((dataKey) => { | Object.keys(skillData.data).forEach((dataKey) => { | ||
// Special case for Township item conversions | // Special case for Township item conversions | ||
if ( | if ( | ||
skillObj[dataKey] !== undefined && | |||
skillData.skillID === 'melvorD:Township' && | |||
dataKey === 'itemConversions' | |||
) { | |||
Object.keys(skillData.data[dataKey]).forEach((convKey) => { | Object.keys(skillData.data[dataKey]).forEach((convKey) => { | ||
skillData.data[dataKey][convKey].forEach((resource) => { | skillData.data[dataKey][convKey].forEach((resource) => { | ||
// Find the resource if it already exists within the combined data | // Find the resource if it already exists within the combined data | ||
const resourceIdx = skillObj[dataKey][convKey].findIndex((res) => res.resourceID === resource.resourceID); | const resourceIdx = skillObj[dataKey][convKey].findIndex( | ||
(res) => res.resourceID === resource.resourceID | |||
); | |||
if (resourceIdx === -1) { | if (resourceIdx === -1) { | ||
skillObj[dataKey][convKey].push(resource); | skillObj[dataKey][convKey].push(resource); | ||
} | } else { | ||
skillObj[dataKey][convKey][resourceIdx].items.push(...resource.items); | skillObj[dataKey][convKey][resourceIdx].items.push(...resource.items); | ||
} | } | ||
}) | }); | ||
}); | }); | ||
} | } else if ( | ||
Array.isArray(skillData.data[dataKey]) && | |||
skillData.data[dataKey].length > 0 && | |||
skillData.data[dataKey][0].insertAt !== undefined | |||
) { | |||
// Data is ordered, special handling applies | // Data is ordered, special handling applies | ||
skillObj[dataKey] = this.combineOrderedData(skillObj[dataKey], skillData.data[dataKey]); | skillObj[dataKey] = this.combineOrderedData(skillObj[dataKey], skillData.data[dataKey]); | ||
} | } else if (skillObj[dataKey] === undefined || !Array.isArray(skillObj[dataKey])) { | ||
// Property is undefined or isn't an array | // Property is undefined or isn't an array | ||
skillObj[dataKey] = skillData.data[dataKey]; | |||
} | } else { | ||
// Property is an array | // Property is an array | ||
skillObj[dataKey].push(...skillData.data[dataKey]); | |||
} | } | ||
}); | }); | ||
Line 511: | Line 593: | ||
break; | break; | ||
default: | default: | ||
console.warn(`Skipping unknown category while registering data package: ${ categoryName }`); | console.warn(`Skipping unknown category while registering data package: ${categoryName}`); | ||
break; | break; | ||
} | } | ||
}); | }); | ||
} | |||
registerDataPackage(namespace) { | |||
// Consolidates the data package identified by namespace with existing data within | |||
// this.gameData | |||
const packData = this.packData[namespace].data; | |||
if (packData === undefined) { | |||
throw new Error(`Couldn't find data for package ${namespace}`); | |||
} | |||
// Add data within the game but outside of data packs | |||
this.registerNonPackData(); | |||
// Consolidate data | |||
this.registerPackData(packData); | |||
// If the data package contains modifications, apply these also | // If the data package contains modifications, apply these also | ||
const modificationData = this.packData[namespace].modifications; | const modificationData = this.packData[namespace].modifications; | ||
Line 520: | Line 614: | ||
this.applyDataModifications(modificationData); | this.applyDataModifications(modificationData); | ||
} | } | ||
// Dependent data is handled later, once all packages have been registered | |||
if ( | |||
if (!this.registeredNamespaces.includes(namespace)) { | |||
this.registeredNamespaces.push(namespace); | |||
} | } | ||
} | } | ||
processDependentData() { | |||
Object.entries(this.packData) | |||
.forEach(([namespace, packData]) => { | |||
if (packData.dependentData !== undefined) { | |||
packData.dependentData.forEach((depDataForNS) => { | |||
const depNS = depDataForNS.namespace; | |||
if (!this.registeredNamespaces.includes(depNS)) { | |||
console.warn( | |||
`Could not apply dependent data from package ${namespace}: Data depends on namespace ${depNS}, which has not been registered` | |||
); | |||
console.warn(`Could not apply data | |||
} | } | ||
else { | else { | ||
console.log(`Attempting to apply dependent data for ${depNS} from package ${namespace}`); | |||
if (depDataForNS.data !== undefined) { | |||
this.registerPackData(depDataForNS.data) | |||
} | |||
if (depDataForNS.modifications !== undefined) { | |||
this.applyDataModifications(depDataForNS.modifications); | |||
} | |||
} | |||
} | } | ||
} | }); | ||
}); | } | ||
}); | |||
} | |||
getDataToModify(modCat) { | |||
switch (modCat) { | |||
case 'combatAreaCategories': | |||
case 'damageTypes': | |||
case 'dungeons': | |||
case 'equipmentSlots': | |||
case 'gamemodes': | |||
case 'items': | |||
case 'modifiers': | |||
case 'pets': | |||
case 'shopUpgradeChains': | |||
case 'shopPurchases': | |||
case 'skillData': | |||
case 'slayerAreas': | |||
return this.gameData[modCat]; | |||
case 'cookingCategories': | |||
const cookingSkill = this.getObjectByID(this.gameData.skillData, 'melvorD:Cooking', 'skillID'); | |||
return cookingSkill.data.categories; | |||
case 'fletchingRecipes': | |||
const fletchingSkill = this.getObjectByID(this.gameData.skillData, 'melvorD:Fletching', 'skillID'); | |||
return fletchingSkill.data.recipes; | |||
} | |||
return undefined; | |||
} | |||
applyModifierModifications(objToModify, adjustments) { | |||
if (objToModify.modifiers === undefined) { | |||
objToModify.modifiers = {}; | |||
} | |||
Object.keys(adjustments) | |||
.forEach((adjType) => { | |||
if (adjType === 'add') { | |||
Object.entries(adjustments[adjType]) | |||
.forEach(([chgKey, chgVal]) => { | |||
if (objToModify.modifiers[chgKey] === undefined) { | |||
objToModify.modifiers[chgKey] = chgVal; | |||
} | |||
else if (Array.isArray(chgVal)) { | |||
objToModify.modifiers[chgKey].push(...chgVal); | |||
} | |||
else { | |||
objToModify.modifiers[chgKey] += chgVal; | |||
} | |||
}); | |||
} | |||
else { | |||
console.warn( | |||
`Could not apply data modification: Unhandled modifier adjustment "${adjType}"` | |||
); | |||
} | |||
} | |||
); | |||
} | |||
applyAddRemoveModifications(objToModify, adjustments, modifyKey) { | |||
if (adjustments.remove !== undefined && Array.isArray(objToModify[modifyKey])) { | |||
// adjustments.remove is an array of requirement types to be removed | |||
let i = 0; | |||
while (i < objToModify[modifyKey].length) { | |||
if (adjustments.remove.includes(objToModify[modifyKey][i].type)) { | |||
objToModify[modifyKey].splice(i, 1); | |||
} | |||
else { | |||
i++; | |||
} | |||
} | |||
} | |||
if (adjustments.add !== undefined) { | |||
if (objToModify[modifyKey] === undefined) { | |||
objToModify[modifyKey] = adjustments.add; | |||
} | } | ||
else | else { | ||
objToModify[modifyKey].push(...adjustments.add); | |||
} | } | ||
} | |||
} | |||
applyGamemodeSpecificModifications(objToModify, adjustments, newProperty) { | |||
const gamemodeID = adjustments.gamemodeID; | |||
if (objToModify.gamemodeOverrides === undefined) { | |||
objToModify.gamemodeOverrides = []; | |||
} | |||
let gamemodeEntryToModify = this.getObjectByID(objToModify.gamemodeOverrides, gamemodeID, 'gamemodeID'); | |||
if (gamemodeEntryToModify === undefined) { | |||
// Initialize gamemode overrides | |||
objToModify.gamemodeOverrides.push({ | |||
gamemodeID: gamemodeID | |||
}); | |||
gamemodeEntryToModify = this.getObjectByID(objToModify.gamemodeOverrides, gamemodeID, 'gamemodeID'); | |||
} | |||
if (gamemodeEntryToModify[newProperty] === undefined) { | |||
gamemodeEntryToModify[newProperty] = structuredClone(objToModify[newProperty]) ?? {}; | |||
} | |||
this.applyAddRemoveModifications(gamemodeEntryToModify, adjustments, newProperty); | |||
} | |||
applyDataModifications(modData) { | |||
const modDataKeys = Object.keys(modData).filter((modCatID) => !this.excludedCategories.includes(modCatID)); | |||
for (const modCatID in modDataKeys) { | |||
const modCat = modDataKeys[modCatID]; | |||
const catData = modData[modCat]; | |||
const dataToModify = this.getDataToModify(modCat); | |||
const modObjIDKey = (modCat === 'skillData' ? 'skillID' : 'id'); | |||
if (dataToModify === undefined) { | |||
console.warn( | |||
`Could not apply data modification for category "${modCat}": Unable to retrieve category data to be modified` | |||
); | |||
} | } | ||
else | else { | ||
catData.forEach((modItem) => { | catData.forEach((modItem) => { | ||
const modObjID = modItem | const modObjID = modItem[modObjIDKey]; | ||
if (modObjID === undefined) { | if (modObjID === undefined) { | ||
console.warn(`Could not apply data modification: ID of object to be modified not found | console.warn( | ||
} | `Could not apply data modification for category "${modCat}": ID of object to be modified not found` | ||
); | |||
const | } else { | ||
if ( | const objToModify = this.getObjectByID(dataToModify, modObjID, modObjIDKey); | ||
console.warn(`Could not apply data modification: Object with ID "${ modObjID }" not found for | if (objToModify === undefined) { | ||
console.warn( | |||
`Could not apply data modification: Object with ID "${modObjID}" not found for ctaegory "${modCat}"` | |||
); | |||
} | } | ||
else { | else { | ||
Object. | switch (modCat) { | ||
if ( | case 'combatAreaCategories': | ||
// | // The 'areas' property of elements within the category data are ordered data | ||
objToModify.areas = this.combineOrderedData(objToModify.areas, modItem.areas.add); | |||
Object.keys( | break; | ||
case 'damageTypes': | |||
Object.entries(modItem) | |||
.filter(([k, v]) => k !== 'id') | |||
.forEach(([k, v]) => { | |||
if (typeof v === 'object' && (v.add !== undefined || v.remove !== undefined)) { | |||
this.applyAddRemoveModifications(objToModify, v, k); | |||
} | |||
else { | |||
console.warn( | |||
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"` | |||
); | |||
} | |||
}); | |||
break; | |||
case 'gamemodes': | |||
Object.entries(modItem) | |||
.filter(([k, v]) => k !== 'id') | |||
.forEach(([k, v]) => { | |||
if (typeof v === 'object' && (v.add !== undefined || v.remove !== undefined)) { | |||
this.applyAddRemoveModifications(objToModify, v, k); | |||
} | |||
else if (['abyssalLevelCapCost', 'post99RollConversion'].includes(k)) { | |||
objToModify[k] = v; | |||
} | |||
else { | |||
console.warn( | |||
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"` | |||
); | |||
} | |||
}); | |||
break; | |||
case 'shopPurchases': | |||
case 'shopUpgradeChains': | |||
// Modify the root upgrade ID of shop upgrade chains, and modify attributes of shop purchases | |||
const overrideKeys = { | |||
purchaseRequirements: { | |||
sourceKey: 'newRequirements', // Key that holds the data in the data package | |||
destKey: 'purchaseRequirementsOverrides', // Key to insert into within this.gameData | |||
subKey: 'requirements', // Sub-key containing the override data | |||
}, | |||
cost: { | |||
sourceKey: 'newCosts', | |||
destKey: 'costOverrides', | |||
subKey: 'cost', | |||
}, | |||
}; | |||
Object.keys(modItem) | |||
.filter((k) => k !== 'id') | |||
.forEach((k) => { | |||
const overrideKey = overrideKeys[k]; | |||
if (overrideKey !== undefined) { | |||
// Is an override specific to a gamemode, do not replace | |||
// the key's existing data | |||
const destKey = overrideKey.destKey; | |||
if (objToModify[destKey] === undefined) { | |||
objToModify[destKey] = []; | |||
} | |||
modItem[k].forEach((gamemodeOverride) => { | |||
var newData = {}; | |||
newData.gamemodeID = gamemodeOverride.gamemodeID; | |||
newData[overrideKey.subKey] = gamemodeOverride[overrideKey.sourceKey]; | |||
objToModify[destKey].push(newData); | |||
}); | |||
} else { | |||
objToModify[k] = modItem[k]; | |||
} | } | ||
}); | |||
break; | |||
if ( | case 'cookingCategories': | ||
// Append to the list of shop upgrade IDs for cooking utilities/categories | |||
case 'fletchingRecipes': | |||
// Append to alternativeCosts property of recipes (e.g. Arrow shafts) | |||
} | Object.keys(modItem) | ||
.filter((k) => k !== 'id') | |||
.forEach((k) => { | |||
if ((k === 'shopUpgradeIDs') || (k === 'alternativeCosts')) { | |||
if (objToModify[k] === undefined) { | |||
objToModify[k] = modItem[k]; | |||
} else { | |||
objToModify[k].push(...modItem[k]); | |||
} | |||
} else { | |||
console.warn( | |||
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"` | |||
); | |||
} | |||
}); | |||
break; | |||
case 'skillData': | |||
Object.entries(modItem.data) | |||
.forEach(([skillProp, propModData]) => { | |||
propModData.forEach((subModItem) => { | |||
const subObjToModify = this.getObjectByID(objToModify.data[skillProp], subModItem.id); | |||
if (subObjToModify === undefined) { | |||
console.warn(`Couldn't find skill object with ID ${subModItem.id} to modify. Property ${skillProp} in skill ID ${objToModify.skillID}`); | |||
} | } | ||
else { | else { | ||
Object.entries(subModItem) | |||
.forEach(([subProp, subData]) => { | |||
if (subProp === 'modifiers') { | |||
this.applyModifierModifications(subObjToModify, subData); | |||
} | |||
else if (subProp !== 'id') { | |||
this.applyAddRemoveModifications(subObjToModify, subData, subProp); | |||
} | |||
}); | |||
} | |||
}); | |||
}); | |||
break; | |||
case 'dungeons': | |||
// Add gamemode specific data to dungeons | |||
Object.keys(modItem) | |||
.filter((k) => k !== 'id') | |||
.forEach((k) => { | |||
if (k === 'gamemodeRewardItemIDs') { | |||
// Add gamemode specific item rewards to dungeon data | |||
const itemRules = modItem[k]; | |||
Object.keys(itemRules).forEach((ruleKey) => { | |||
if (ruleKey === 'add') { | |||
itemRules[ruleKey].forEach((itemDef) => { | |||
const modToApply = { | |||
gamemodeID: itemDef.gamemodeID, | |||
add: itemDef.rewardItemIDs | |||
} | |||
this.applyGamemodeSpecificModifications(objToModify, modToApply, 'rewardItemIDs'); | |||
}); | |||
} else { | |||
console.warn( | |||
`Could not apply data modification: Unknown rule for gamemode item rewards: "${ruleKey}", object "${modObjID}"` | |||
); | |||
} | |||
}); | |||
} else if (k === 'gamemodeEntryRequirements') { | |||
// Add or remove gamemode specific entry requirements to dungeon data | |||
this.applyGamemodeSpecificModifications(objToModify, modItem[k], 'entryRequirements'); | |||
} else { | |||
console.warn( | |||
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"` | |||
); | |||
} | |||
}); | |||
break; | |||
case 'modifiers': | |||
// Add modifier aliases to existing mod scopes | |||
if (objToModify.allowedScopes === undefined) { | |||
console.warn(`Could not apply data modification: Modifier with ID ${modObjID} not found or modifier has no scopes`); | |||
} else { | |||
modItem.allowedScopes.forEach((srcScope) => { | |||
// Find scope within modifier objToModify with matching scopes definition | |||
const srcScopeKeys = Object.keys(srcScope.scopes); | |||
objToModify.allowedScopes.forEach((destScope) => { | |||
const destScopeKeys = Object.keys(destScope.scopes); | |||
const scopeMatch = ( | |||
srcScopeKeys.length === destScopeKeys.length | |||
&& srcScopeKeys.every((k) => destScope.scopes[k] !== undefined && srcScope.scopes[k] == destScope.scopes[k]) | |||
); | |||
if (scopeMatch) { | |||
// Scopes match - add aliases to modifier allowedScope definition | |||
const aliasKeys = ['posAliases', 'negAliases']; | |||
aliasKeys.forEach((aliasKey) => { | |||
if (srcScope[aliasKey] !== undefined) { | |||
if (destScope[aliasKey] === undefined) { | |||
destScope[aliasKey] = []; | |||
} | |||
destScope[aliasKey].push(...srcScope[aliasKey]); | |||
} | |||
}); | |||
} | } | ||
}); | }); | ||
}); | |||
} | |||
break; | |||
case 'items': | |||
Object.keys(modItem) | |||
.filter((k) => k !== 'id') | |||
.forEach((k) => { | |||
if (k === 'modifiers') { | |||
this.applyModifierModifications(objToModify, modItem[k]); | |||
} | |||
else if (k === 'consumesOn') { | |||
Object.keys(modItem[k]) | |||
.forEach((adjType) => { | |||
if (adjType === 'add') { | |||
if (objToModify[k] === undefined) { | |||
objToModify[k] = modItem[k][adjType]; | |||
} | |||
else { | |||
objToModify[k].push(...modItem[k][adjType]); | |||
} | |||
} | |||
else { | |||
console.warn( | |||
`Could not apply data modification: Unhandled adjustment type "${adjType}" for category "${modCat}", object "${modObjID}, property ${k}"` | |||
); | |||
} | |||
}); | |||
} | |||
else { | |||
console.warn( | |||
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"` | |||
); | |||
} | |||
} | } | ||
else { | ); | ||
break; | |||
case 'pets': | |||
Object.keys(modItem) | |||
.filter((k) => k !== 'id') | |||
.forEach((k) => { | |||
if (k === 'modifiers') { | |||
this.applyModifierModifications(objToModify, modItem[k]); | |||
} | |||
else { | |||
console.warn( | |||
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"` | |||
); | |||
} | |||
} | } | ||
); | |||
break; | |||
case 'equipmentSlots': | |||
Object.keys(modItem) | |||
.filter((k) => k !== 'id') | |||
.forEach((k) => { | |||
if (k === 'requirements') { | |||
this.applyAddRemoveModifications(objToModify, modItem[k], 'requirements'); | |||
} | |||
else { | |||
console.warn( | |||
} | `Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"` | ||
} | ); | ||
} | |||
} | |||
); | |||
break; | |||
case 'slayerAreas': | |||
Object.keys(modItem) | |||
.filter((k) => k !== 'id') | |||
.forEach((k) => { | |||
if (k === 'gamemodeEntryRequirements') { | |||
this.applyGamemodeSpecificModifications(objToModify, modItem[k], 'entryRequirements'); | |||
} | |||
else { | |||
console.warn( | |||
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"` | |||
); | |||
} | |||
} | |||
); | |||
break; | |||
default: | |||
console.warn( | |||
`Could not apply data modification: Unhandled category "${modCat}"` | |||
); | |||
} | |||
} | } | ||
} | } | ||
}); | }); | ||
} | } | ||
} | } | ||
Line 708: | Line 1,032: | ||
registerNonPackData() { | registerNonPackData() { | ||
// Some data resides outside of packages. Add any such data to this.gameData within this function | // Some data resides outside of packages. Add any such data to this.gameData within this function | ||
// Metadata for data/file version | |||
if (this.gameData._dataVersion === undefined) { | |||
this.gameData._dataVersion = ({ | |||
gameVersion: this.getGameVersion().substring(1), | |||
fileVersion: this.getGameFileVersion().substring(1) | |||
}); | |||
} | |||
// Namespaces | |||
if (this.gameData.namespaces === undefined) { | if (this.gameData.namespaces === undefined) { | ||
const nsData = []; | const nsData = []; | ||
game.registeredNamespaces.forEach((ns) => { | game.registeredNamespaces.forEach((ns) => { | ||
if (ns.isModded) { | if (ns.isModded) { | ||
throw new Error(`Modded namespace '${ ns.displayName }' found, all mods must be disabled before game data can be generated`); | throw new Error( | ||
} | `Modded namespace '${ns.displayName}' found, all mods must be disabled before game data can be generated` | ||
); | |||
} else { | |||
nsData.push(ns); | nsData.push(ns); | ||
} | } | ||
Line 720: | Line 1,053: | ||
this.gameData.namespaces = nsData; | this.gameData.namespaces = nsData; | ||
} | } | ||
if (this.gameData. | if (this.gameData.currencies === undefined) { | ||
this.gameData.currencies = game.currencies.allObjects.map((c) => ({ | |||
id: c.id, | |||
name: c.name, | |||
type: c.type | |||
})); | |||
} | |||
this.gameData. | // Melvor realm exists outside of data packages | ||
if (this.gameData.realms === undefined) { | |||
this.gameData.realms = game.realms | |||
.filter((r) => r.id === 'melvorD:Melvor') | |||
.map((r) => ({ | |||
id: r.id, | |||
name: r.name, | |||
unlockRequirements: r.unlockRequirements | |||
})); | |||
} | } | ||
if (this.gameData. | // Normal damage type exists outside of data packages | ||
this.gameData. | if (this.gameData.damageTypes === undefined) { | ||
this.gameData.damageTypes = game.damageTypes | |||
.filter((d) => d.id === 'melvorD:Normal') | |||
.map((d) => ({ | |||
id: d.id, | |||
name: d.name, | |||
resistanceCap: d._resistanceCap, | |||
resistanceName: d.resistanceName | |||
})); | |||
} | } | ||
if (this.gameData.combatAreaDifficulties === undefined) { | if (this.gameData.combatAreaDifficulties === undefined) { | ||
this.gameData.combatAreaDifficulties = CombatAreaMenuElement.difficulty.map((i) => i.name); | this.gameData.combatAreaDifficulties = CombatAreaMenuElement.difficulty.map((i) => i.name); | ||
} | } | ||
if (this.gameData.attackTypes === undefined) { | if (this.gameData.attackTypes === undefined) { | ||
this.gameData.attackTypes = AttackTypeID; | this.gameData.attackTypes = AttackTypeID; | ||
} | } | ||
} | } | ||
Line 779: | Line 1,094: | ||
if (existingData === undefined) { | if (existingData === undefined) { | ||
resultData = []; | resultData = []; | ||
} | } else { | ||
resultData = structuredClone(existingData); | resultData = structuredClone(existingData); | ||
} | } | ||
newData.forEach((orderData) => { | newData.forEach((orderData) => { | ||
switch(orderData.insertAt) { | switch (orderData.insertAt) { | ||
case 'Start': | case 'Start': | ||
resultData.splice(0, 0, ...orderData.ids); | resultData.splice(0, 0, ...orderData.ids); | ||
Line 794: | Line 1,108: | ||
const beforeIdx = resultData.findIndex((item) => item === orderData.beforeID); | const beforeIdx = resultData.findIndex((item) => item === orderData.beforeID); | ||
if (beforeIdx === -1) { | if (beforeIdx === -1) { | ||
throw new Error(`Couldn't insert before: Item ${ orderData.beforeID } is not in the array.`); | throw new Error(`Couldn't insert before: Item ${orderData.beforeID} is not in the array.`); | ||
} | } | ||
resultData.splice(beforeIdx, 0, ...orderData.ids); | resultData.splice(beforeIdx, 0, ...orderData.ids); | ||
Line 801: | Line 1,115: | ||
const afterIdx = resultData.findIndex((item) => item === orderData.afterID); | const afterIdx = resultData.findIndex((item) => item === orderData.afterID); | ||
if (afterIdx === -1) { | if (afterIdx === -1) { | ||
throw new Error(`Couldn't insert after: Item ${ orderData.afterID } is not in the array.`); | throw new Error(`Couldn't insert after: Item ${orderData.afterID} is not in the array.`); | ||
} | } | ||
resultData.splice(afterIdx + 1, 0, ...orderData.ids); | resultData.splice(afterIdx + 1, 0, ...orderData.ids); | ||
Line 813: | Line 1,127: | ||
// Returns true if the property is to be removed, false if it is to be retained | // Returns true if the property is to be removed, false if it is to be retained | ||
isPropertyFiltered(entityType, entity, propertyName) { | isPropertyFiltered(entityType, entity, propertyName) { | ||
switch(propertyName) { | switch (propertyName) { | ||
case 'media': | case 'media': | ||
case 'altMedia': | case 'altMedia': | ||
Line 840: | Line 1,154: | ||
if (entityType === 'items') { | if (entityType === 'items') { | ||
return entity.tier === 'none'; | return entity.tier === 'none'; | ||
} | } else { | ||
return false; | return false; | ||
} | } | ||
Line 852: | Line 1,165: | ||
// Returns undefined if the property has no transformation | // Returns undefined if the property has no transformation | ||
transformProperty(entityType, entity, propertyName, namespace) { | transformProperty(entityType, entity, propertyName, namespace) { | ||
switch(propertyName) { | switch (propertyName) { | ||
case 'langHint': | case 'langHint': | ||
case 'langCustomDescription': | case 'langCustomDescription': | ||
Line 859: | Line 1,172: | ||
const newStats = {}; | const newStats = {}; | ||
entity.forEach((stat) => { | entity.forEach((stat) => { | ||
if ( | let statKey = stat.key; | ||
if (stat.damageType !== undefined) { | |||
statKey += this.getLocalID(stat.damageType); | |||
} | } | ||
if (newStats[statKey] === undefined) { | |||
newStats[stat. | newStats[statKey] = stat.value; | ||
} else { | |||
newStats[statKey] += stat.value; | |||
} | } | ||
}); | }); | ||
Line 870: | Line 1,186: | ||
if (entityType !== 'skillData') { | if (entityType !== 'skillData') { | ||
return undefined; | return undefined; | ||
} | } else { | ||
const newData = structuredClone(entity); | const newData = structuredClone(entity); | ||
newData.forEach((i) => { | newData.forEach((i) => { | ||
Line 883: | Line 1,198: | ||
} | } | ||
langApply(parentNode, nodeKey, isSkill) { | langApply(parentNode, nodeKey, isSkill) { | ||
const nodeName = | const nodeName = isSkill ? parentNode[nodeKey].skillID : nodeKey; | ||
const altMagicDescIDKey = function(data) { | const altMagicDescIDKey = function (data) { | ||
// Accepts an Alt. Magic spell object, returns the ID format for that spell | // Accepts an Alt. Magic spell object, returns the ID format for that spell | ||
// Using a function for this as some spells (e.g. Superheat) have bespoke logic | // Using a function for this as some spells (e.g. Superheat) have bespoke logic | ||
Line 891: | Line 1,206: | ||
return 'HOLY_INVOCATION'; | return 'HOLY_INVOCATION'; | ||
} | } | ||
switch(data.specialCost.type) { | switch (data.specialCost.type) { | ||
case 'BarIngredientsWithCoal': | case 'BarIngredientsWithCoal': | ||
return 'SUPERHEAT'; | return 'SUPERHEAT'; | ||
Line 910: | Line 1,225: | ||
chainName: 'chainNameLang', | chainName: 'chainNameLang', | ||
defaultDescription: 'descriptionLang', | defaultDescription: 'descriptionLang', | ||
defaultName: 'defaultNameLang' | defaultName: 'defaultNameLang', | ||
}; | }; | ||
const langPropName = propToLang[dataKey]; | const langPropName = propToLang[dataKey]; | ||
Line 919: | Line 1,234: | ||
} | } | ||
} | } | ||
} | }; | ||
const itemDesc = (data) => { | const itemDesc = (data) => { | ||
const item = game.items.getObjectByID(data.id); | const item = game.items.getObjectByID(data.id); | ||
if (item !== undefined) { | if (item !== undefined && item.hasDescription) { | ||
return item.description; | |||
} else return ''; | |||
// | }; | ||
const shopPurchaseDesc = (data) => { | |||
const purchase = game.shop.purchases.getObjectByID(data.id); | |||
if (purchase !== undefined) { | |||
// Logic taken from description method of ShopPurchase class & slightly modified | |||
// to avoid retrieving an item's modified description, which can include HTML | |||
let desc = ''; | |||
if (purchase._customDescription !== undefined) { | |||
if (purchase.isModded) { | |||
return purchase._customDescription; | |||
} | |||
else { | |||
return getLangString(`SHOP_DESCRIPTION_${ purchase.localID }`); | |||
} | |||
} | |||
if (purchase.contains.itemCharges !== undefined) { | |||
return purchase.contains.itemCharges.item.description; | |||
} | |||
if (purchase.contains.items.length === 1) { | |||
return purchase.contains.items[0].item.description; // Was modifiedDescription | |||
} | |||
if (purchase.contains.pet !== undefined) { | |||
return purchase.contains.pet.description; | |||
} | } | ||
if (purchase.contains.stats !== undefined) { | |||
desc = purchase.contains.stats.describePlain(); | |||
} | } | ||
if (purchase.hasDisabledModifier) { | |||
desc += getLangString('MENU_TEXT_CONTAINS_DISABLED_MODIFIER'); | |||
} | } | ||
} | return desc; | ||
} | } else return ''; | ||
}; | |||
const relicDesc = (data) => { | const relicDesc = (data) => { | ||
const relic = game.ancientRelics.getObjectByID(data.id); | const relic = game.ancientRelics.getObjectByID(data.id); | ||
Line 946: | Line 1,278: | ||
return relic.name; | return relic.name; | ||
} | } | ||
} | }; | ||
const passiveDesc = (data) => { | const passiveDesc = (data) => { | ||
const passive = game.combatPassives.getObjectByID(data.id); | const passive = game.combatPassives.getObjectByID(data.id); | ||
Line 952: | Line 1,284: | ||
return passive.description; | return passive.description; | ||
} | } | ||
} | }; | ||
const spAttDesc = (data) => { | const spAttDesc = (data) => { | ||
const spAtt = game.specialAttacks.getObjectByID(data.id); | const spAtt = game.specialAttacks.getObjectByID(data.id); | ||
Line 958: | Line 1,290: | ||
return spAtt.description; | return spAtt.description; | ||
} | } | ||
} | }; | ||
const tsWorshipName = (data) => { | const tsWorshipName = (data) => { | ||
const worship = game.township.worships.getObjectByID(data.id); | const worship = game.township.worships.getObjectByID(data.id); | ||
Line 964: | Line 1,296: | ||
return worship.name; | return worship.name; | ||
} | } | ||
} | }; | ||
const tsWorshipStatueName = (data) => { | const tsWorshipStatueName = (data) => { | ||
const worship = game.township.worships.getObjectByID(data.id); | const worship = game.township.worships.getObjectByID(data.id); | ||
Line 970: | Line 1,302: | ||
return worship.statueName; | return worship.statueName; | ||
} | } | ||
} | }; | ||
const attackSpellbooksName = (data) => { | |||
const book = game.attackSpellbooks.getObjectByID(data.id); | |||
if (book !== undefined) { | |||
return book.name; | |||
} | |||
}; | |||
const attackSpellName = (data) => { | |||
const spell = game.attackSpells.getObjectByID(data.id); | |||
if (spell !== undefined) { | |||
return this.getLangString(`${ spell.spellbook.spellNameLangPrefix }${ spell.localID }`); | |||
} | |||
}; | |||
const hasNoLangData = [ | const hasNoLangData = [ | ||
// Categories that contain no localized text. Supresses warnings about no lang data | // Categories that contain no localized text. Supresses warnings about no lang data | ||
'ancientRelicsDisplayOrder', | |||
'bankSortOrder', | 'bankSortOrder', | ||
'combatAreaDisplayOrder', | 'combatAreaDisplayOrder', | ||
'combatAreaCategoryOrder', | |||
'combatEffectTables', | |||
'combatEffectTemplates', | |||
'combatEvents', | 'combatEvents', | ||
'dungeonDisplayOrder', | 'dungeonDisplayOrder', | ||
Line 982: | Line 1,330: | ||
'itemUpgrades', | 'itemUpgrades', | ||
'itmMonsters', | 'itmMonsters', | ||
'modifiers', // TODO Does have lang data, supressing warning for now | |||
'randomAbyssalGems', | |||
'randomFiremakingOils', | |||
'randomFragments', | |||
'randomGems', | 'randomGems', | ||
'randomSuperiorGems', | 'randomSuperiorGems', | ||
'slayerAreaDisplayOrder', | 'slayerAreaDisplayOrder', | ||
'slayerTaskCategories', // TODO Does have lang data, supressing warning for now | |||
'shopCategoryOrder', | 'shopCategoryOrder', | ||
'shopDisplayOrder', | 'shopDisplayOrder', | ||
'skillLevelCapIncreases', | |||
'skillTreesDisplayOrder', | |||
'spiderLairMonsters', | 'spiderLairMonsters', | ||
'stackingEffects' | 'stackingEffects', | ||
]; | ]; | ||
const langKeys = { | const langKeys = { | ||
realms: { | |||
name: { stringSpecial: ' | name: { key: 'REALM', idFormat: 'NAME_{ID}' }, | ||
}, | |||
damageTypes: { | |||
name: { idFormat: 'DAMAGE_TYPE_{ID}' }, | |||
}, | |||
combatTriangleSets: { | |||
name: { key: 'COMBAT_TRIANGLE_NAME', idFormat: 'NAME_{ID}' }, | |||
}, | |||
attackSpellbooks: { | |||
name: { stringSpecial: 'attackSpellbooksName' }, | |||
}, | }, | ||
attackSpells: { | |||
name: { | name: { stringSpecial: 'attackSpellName' }, | ||
}, | }, | ||
ancientRelics: { | |||
name: { | name: { stringSpecial: 'relicDesc' }, | ||
}, | }, | ||
attackStyles: { | attackStyles: { | ||
name: { key: 'COMBAT_MISC', idFormat: 'ATTACK_STYLE_NAME_{ID}' } | name: { key: 'COMBAT_MISC', idFormat: 'ATTACK_STYLE_NAME_{ID}' }, | ||
}, | }, | ||
attacks: { | attacks: { | ||
name: { key: 'SPECIAL_ATTACK_NAME' }, | name: { key: 'SPECIAL_ATTACK_NAME' }, | ||
description: { stringSpecial: 'spAttDesc' } | description: { stringSpecial: 'spAttDesc' }, | ||
}, | }, | ||
auroraSpells: { | auroraSpells: { | ||
name: { key: 'MAGIC', idFormat: 'AURORA_NAME_{ID}' } | name: { key: 'MAGIC', idFormat: 'AURORA_NAME_{ID}' }, | ||
}, | |||
combatAreaCategories: { | |||
name: { key: 'COMBAT_AREA_CATEGORY' } | |||
}, | }, | ||
combatAreas: { | combatAreas: { | ||
name: { key: 'COMBAT_AREA', idFormat: 'NAME_{ID}'} | name: { key: 'COMBAT_AREA', idFormat: 'NAME_{ID}' }, | ||
}, | |||
combatEffectGroups: { | |||
name: { idKey: 'nameLang' } | |||
}, | |||
combatEffects: { | |||
name: { idKey: 'nameLang' } | |||
}, | }, | ||
combatPassives: { | combatPassives: { | ||
name: { key: 'PASSIVES', idFormat: 'NAME_{ID}' }, | name: { key: 'PASSIVES', idFormat: 'NAME_{ID}' }, | ||
customDescription: { stringSpecial: 'passiveDesc' } | customDescription: { stringSpecial: 'passiveDesc' }, | ||
//customDescription: { key: 'PASSIVES', idFormat: 'DESC_{ID}' } | //customDescription: { key: 'PASSIVES', idFormat: 'DESC_{ID}' } | ||
}, | }, | ||
curseSpells: { | curseSpells: { | ||
name: { key: 'MAGIC', idFormat: 'CURSE_NAME_{ID}' } | name: { key: 'MAGIC', idFormat: 'CURSE_NAME_{ID}' }, | ||
}, | }, | ||
dungeons: { | dungeons: { | ||
name: { key: 'DUNGEON', idFormat: 'NAME_{ID}' } | name: { key: 'DUNGEON', idFormat: 'NAME_{ID}' }, | ||
}, | |||
abyssDepths: { | |||
name: { key: 'THE_ABYSS', idFormat: 'NAME_{ID}' }, | |||
}, | |||
strongholds: { | |||
name: { idFormat: 'STRONGHOLD_NAME_{ID}' }, | |||
}, | |||
equipmentSlots: { | |||
emptyName: { idFormat: 'EQUIP_SLOT_{ID}' } | |||
}, | }, | ||
gamemodes: { | gamemodes: { | ||
Line 1,028: | Line 1,410: | ||
description: { key: 'GAMEMODES', idFormat: 'GAMEMODE_DESC_{ID}' }, | description: { key: 'GAMEMODES', idFormat: 'GAMEMODE_DESC_{ID}' }, | ||
// Gamemodes have an array of rules | // Gamemodes have an array of rules | ||
rules: { key: 'GAMEMODES', idFormat: 'GAMEMODE_RULES_{ID}_{NUM}' } | rules: { key: 'GAMEMODES', idFormat: 'GAMEMODE_RULES_{ID}_{NUM}' }, | ||
}, | }, | ||
items: { | items: { | ||
name: { key: 'ITEM_NAME' }, | name: { key: 'ITEM_NAME' }, | ||
customDescription: { stringSpecial: 'itemDesc', onlyIfExists: | customDescription: { stringSpecial: 'itemDesc', onlyIfExists: false }, | ||
}, | }, | ||
lore: { | lore: { | ||
title: { key: 'LORE', idFormat: 'TITLE_{ID}' } | title: { key: 'LORE', idFormat: 'TITLE_{ID}' }, | ||
}, | }, | ||
monsters: { | monsters: { | ||
name: { key: 'MONSTER_NAME' }, | name: { key: 'MONSTER_NAME' }, | ||
description: { key: 'MONSTER_DESCRIPTION' } | description: { key: 'MONSTER_DESCRIPTION' }, | ||
}, | }, | ||
pets: { | pets: { | ||
name: { key: 'PET_NAME' } | name: { key: 'PET_NAME' }, | ||
hint: { idKey: 'langHint' } | |||
}, | }, | ||
prayers: { | prayers: { | ||
name: { key: 'PRAYER', idFormat: 'PRAYER_NAME_{ID}' } | name: { key: 'PRAYER', idFormat: 'PRAYER_NAME_{ID}' }, | ||
}, | }, | ||
shopCategories: { | shopCategories: { | ||
name: { key: 'SHOP_CAT' } | name: { key: 'SHOP_CAT' }, | ||
}, | }, | ||
shopPurchases: { | shopPurchases: { | ||
customName: { key: 'SHOP_NAME', onlyIfExists: true }, | customName: { key: 'SHOP_NAME', onlyIfExists: true }, | ||
customDescription: { | customDescription: { stringSpecial: 'shopPurchaseDesc', onlyIfExists: false }, | ||
}, | }, | ||
shopUpgradeChains: { | shopUpgradeChains: { | ||
chainName: { keySpecial: 'shopChainKey', idSpecial: 'shopChainID' }, | chainName: { keySpecial: 'shopChainKey', idSpecial: 'shopChainID' }, | ||
defaultDescription: { keySpecial: 'shopChainKey', idSpecial: 'shopChainID' }, | defaultDescription: { keySpecial: 'shopChainKey', idSpecial: 'shopChainID' }, | ||
defaultName: { keySpecial: 'shopChainKey', idSpecial: 'shopChainID' } | defaultName: { keySpecial: 'shopChainKey', idSpecial: 'shopChainID' }, | ||
}, | }, | ||
slayerAreas: { | slayerAreas: { | ||
name: { key: 'SLAYER_AREA', idFormat: 'NAME_{ID}' }, | name: { key: 'SLAYER_AREA', idFormat: 'NAME_{ID}' }, | ||
areaEffectDescription: { key: 'SLAYER_AREA', idFormat: 'EFFECT_{ID}' | areaEffectDescription: { key: 'SLAYER_AREA', idFormat: 'EFFECT_{ID}' }, | ||
}, | }, | ||
skillData: { | skillData: { | ||
Line 1,074: | Line 1,453: | ||
// for all skills | // for all skills | ||
_root: { | _root: { | ||
name: { key: 'SKILL_NAME', idFormat: '{SKILLID}' | name: { key: 'SKILL_NAME', idFormat: '{SKILLID}' }, | ||
}, | }, | ||
customMilestones: { | customMilestones: { | ||
name: { key: 'MILESTONES', idKey: 'milestoneID' } | name: { key: 'MILESTONES', idKey: 'milestoneID' }, | ||
}, | }, | ||
masteryLevelUnlocks: { | masteryLevelUnlocks: { | ||
description: { key: 'MASTERY_BONUS', idKey: 'descriptionID', idFormat: '{SKILLID}_{ID}' } | description: { key: 'MASTERY_BONUS', idKey: 'descriptionID', idFormat: '{SKILLID}_{ID}' }, | ||
} | }, | ||
}, | }, | ||
Archaeology: { | Archaeology: { | ||
digSites: { | digSites: { | ||
name: { key: 'POI_NAME_Melvor' } | name: { key: 'POI_NAME_Melvor' }, | ||
} | }, | ||
// TODO Tool names | // TODO Tool names | ||
}, | }, | ||
Agility: { | Agility: { | ||
elitePillars: { | elitePillars: { | ||
name: { key: 'AGILITY', idFormat: 'PILLAR_NAME_{ID}' } | name: { key: 'AGILITY', idFormat: 'PILLAR_NAME_{ID}' }, | ||
}, | }, | ||
obstacles: { | obstacles: { | ||
name: { key: 'AGILITY', idFormat: 'OBSTACLE_NAME_{ID}' } | name: { key: 'AGILITY', idFormat: 'OBSTACLE_NAME_{ID}' }, | ||
}, | }, | ||
pillars: { | pillars: { | ||
name: { key: 'AGILITY', idFormat: 'PILLAR_NAME_{ID}' } | name: { key: 'AGILITY', idFormat: 'PILLAR_NAME_{ID}' }, | ||
} | }, | ||
}, | }, | ||
Astrology: { | Astrology: { | ||
recipes: { | recipes: { | ||
name: { key: 'ASTROLOGY', idFormat: 'NAME_{ID}' } | name: { key: 'ASTROLOGY', idFormat: 'NAME_{ID}' }, | ||
} | }, | ||
}, | }, | ||
Cartography: { | Cartography: { | ||
mapPortals: { _handler: 'mapPortals' }, | mapPortals: { _handler: 'mapPortals' }, | ||
travelEvents: { | travelEvents: { | ||
description: { key: 'TRAVEL_EVENT' } | description: { key: 'TRAVEL_EVENT' }, | ||
}, | }, | ||
worldMaps: { _handler: 'cartoMaps' } | worldMaps: { _handler: 'cartoMaps' }, | ||
//name: { key: 'WORLD_MAP_NAME' }, | |||
//pointsOfInterest: { _handler: 'mapPOI' } | |||
//name: { key: 'POI_NAME', idFormat: '{MAPID}_{ID}' }, | |||
//description: { key: 'POI_DESCRIPTION', idFormat: '{MAPID}_{ID}' } | |||
}, | |||
Cooking: { | |||
categories: { | |||
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'} | |||
} | |||
}, | |||
Crafting: { | |||
categories: { | |||
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'} | |||
} | |||
}, | }, | ||
Farming: { | Farming: { | ||
categories: { | categories: { | ||
description: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_description' }, | description: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_description' }, | ||
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'}, | |||
seedNotice: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_seedNotice' }, | seedNotice: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_seedNotice' }, | ||
singularName: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_singular' } | singularName: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_singular' }, | ||
}, | |||
}, | |||
Fletching: { | |||
categories: { | |||
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'} | |||
} | } | ||
}, | }, | ||
Fishing: { | Fishing: { | ||
areas: { | areas: { | ||
name: { key: 'FISHING', idFormat: 'AREA_NAME_{ID}' } | name: { key: 'FISHING', idFormat: 'AREA_NAME_{ID}' }, | ||
} | }, | ||
}, | }, | ||
Herblore: { | Herblore: { | ||
categories: { | |||
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'} | |||
}, | |||
recipes: { | recipes: { | ||
name: { key: 'POTION_NAME' } | name: { key: 'POTION_NAME' }, | ||
} | }, | ||
}, | }, | ||
Magic: { | Magic: { | ||
altSpells: { | altSpells: { | ||
name: { key: 'MAGIC', idFormat: 'ALTMAGIC_NAME_{ID}' }, | name: { key: 'MAGIC', idFormat: 'ALTMAGIC_NAME_{ID}' }, | ||
description: { key: 'MAGIC', idSpecial: 'altMagicDesc' } | description: { key: 'MAGIC', idSpecial: 'altMagicDesc' }, | ||
} | }, | ||
}, | }, | ||
Mining: { | Mining: { | ||
categories: { | |||
name: { idFormat: 'MINING_TYPE_{ID}' } | |||
}, | |||
rockData: { | rockData: { | ||
name: { key: 'ORE_NAME' } | name: { key: 'ORE_NAME' }, | ||
}, | |||
}, | |||
Runecrafting: { | |||
categories: { | |||
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'} | |||
} | } | ||
}, | }, | ||
Summoning: { | Summoning: { | ||
categories: { | |||
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'} | |||
}, | |||
synergies: { | synergies: { | ||
customDescription: { key: 'SUMMONING_SYNERGY', idKey: 'summonIDs', idFormat: 'DESC_{ID0}_{ID1}', onlyIfExists: true } | customDescription: { | ||
} | key: 'SUMMONING_SYNERGY', | ||
idKey: 'summonIDs', | |||
idFormat: 'DESC_{ID0}_{ID1}', | |||
onlyIfExists: true, | |||
}, | |||
}, | |||
}, | }, | ||
Thieving: { | Thieving: { | ||
areas: { | areas: { | ||
name: { key: 'THIEVING', idFormat: 'AREA_NAME_{ID}' } | name: { key: 'THIEVING', idFormat: 'AREA_NAME_{ID}' }, | ||
}, | }, | ||
npcs: { | npcs: { | ||
name: { key: 'THIEVING', idFormat: 'NPC_NAME_{ID}' } | name: { key: 'THIEVING', idFormat: 'NPC_NAME_{ID}' }, | ||
} | }, | ||
}, | }, | ||
Township: { | Township: { | ||
biomes: { | biomes: { | ||
// Can't locate biome description localization, don't think this is exposed in game UI | // Can't locate biome description localization, don't think this is exposed in game UI | ||
name: { key: 'TOWNSHIP', idFormat: 'BIOME_{ID}' } | name: { key: 'TOWNSHIP', idFormat: 'BIOME_{ID}' }, | ||
}, | }, | ||
buildings: { | buildings: { | ||
// Building description has no localization, as it is unused | // Building description has no localization, as it is unused | ||
name: { key: 'TOWNSHIP', idFormat: 'BUILDING_{ID}' } | name: { key: 'TOWNSHIP', idFormat: 'BUILDING_{ID}' }, | ||
}, | }, | ||
jobs: { | jobs: { | ||
name: { key: 'TOWNSHIP', idFormat: 'JOB_{ID}' } | name: { key: 'TOWNSHIP', idFormat: 'JOB_{ID}' }, | ||
}, | }, | ||
resources: { | resources: { | ||
name: { key: 'TOWNSHIP', idFormat: 'RESOURCE_{ID}' } | name: { key: 'TOWNSHIP', idFormat: 'RESOURCE_{ID}' }, | ||
}, | }, | ||
tasks: { | tasks: { | ||
// name is not exposed in game UI, and has no localization | // name is not exposed in game UI, and has no localization | ||
// category is localized in transformDataNode | // category is localized in transformDataNode | ||
description: { key: 'TOWNSHIP_TASKS', idFormat: '{ID}_description' } | description: { key: 'TOWNSHIP_TASKS', idFormat: '{ID}_description' }, | ||
}, | }, | ||
worships: { | worships: { | ||
name: { stringSpecial: 'tsWorshipName' }, | name: { stringSpecial: 'tsWorshipName' }, | ||
statueName: { stringSpecial: 'tsWorshipStatueName' } | statueName: { stringSpecial: 'tsWorshipStatueName' }, | ||
} | }, | ||
}, | }, | ||
Woodcutting: { | Woodcutting: { | ||
trees: { | trees: { | ||
name: { key: 'TREE_NAME' } | name: { key: 'TREE_NAME' }, | ||
} | }, | ||
} | }, | ||
} | }, | ||
}; | }; | ||
Line 1,215: | Line 1,626: | ||
} | } | ||
langKeyData = langSkill; | langKeyData = langSkill; | ||
} | } else if (langKeys[nodeKey] !== undefined) { | ||
langKeyData = { _root: langKeys[nodeKey] }; | langKeyData = { _root: langKeys[nodeKey] }; | ||
} | } else if (!hasNoLangData.includes(nodeKey)) { | ||
console.warn('No lang key data found for ' + nodeKey); | console.warn('No lang key data found for ' + nodeKey); | ||
} | } | ||
Line 1,229: | Line 1,638: | ||
} | } | ||
if (!Array.isArray(dataToTranslate)) { | if (!Array.isArray(dataToTranslate)) { | ||
dataToTranslate = [ dataToTranslate ]; | dataToTranslate = [dataToTranslate]; | ||
} | } | ||
dataToTranslate.forEach((tData) => { | dataToTranslate.forEach((tData) => { | ||
Object.keys(langKeyData).forEach((langKey) => { | Object.keys(langKeyData).forEach((langKey) => { | ||
const targetData = | const targetData = langKey === '_root' ? tData : tData[langKey]; | ||
if (targetData !== undefined) { | if (targetData !== undefined) { | ||
const targetArr = | const targetArr = Array.isArray(targetData) ? targetData : [targetData]; | ||
targetArr.forEach((target) => { | targetArr.forEach((target) => { | ||
const handlerFunc = langKeyData[langKey]['_handler']; | const handlerFunc = langKeyData[langKey]['_handler']; | ||
if (handlerFunc !== undefined) { | if (handlerFunc !== undefined) { | ||
switch(handlerFunc) { | switch (handlerFunc) { | ||
case 'mapPortals': | case 'mapPortals': | ||
Object.keys(target).forEach((portalKey) => { | Object.keys(target).forEach((portalKey) => { | ||
Line 1,260: | Line 1,669: | ||
break; | break; | ||
} | } | ||
} | } else { | ||
Object.keys(langKeyData[langKey]).forEach((langPropID) => { | Object.keys(langKeyData[langKey]).forEach((langPropID) => { | ||
const langProp = langKeyData[langKey][langPropID]; | const langProp = langKeyData[langKey][langPropID]; | ||
Line 1,270: | Line 1,678: | ||
// The ID key can sometimes be an array of IDs (e.g. Summoning synergies) | // The ID key can sometimes be an array of IDs (e.g. Summoning synergies) | ||
langIDValue = target[langIDKey].map((id) => this.getLocalID((id ?? '').toString())); | langIDValue = target[langIDKey].map((id) => this.getLocalID((id ?? '').toString())); | ||
} | } else { | ||
langIDValue = this.getLocalID((target[langIDKey] ?? '').toString()); | langIDValue = this.getLocalID((target[langIDKey] ?? '').toString()); | ||
} | } | ||
Line 1,277: | Line 1,684: | ||
if (langProp.idSpecial !== undefined) { | if (langProp.idSpecial !== undefined) { | ||
// Use a special method to determine the ID format | // Use a special method to determine the ID format | ||
switch(langProp.idSpecial) { | switch (langProp.idSpecial) { | ||
case 'altMagicDesc': | case 'altMagicDesc': | ||
langIdent = altMagicDescIDKey(target); | langIdent = altMagicDescIDKey(target); | ||
Line 1,288: | Line 1,695: | ||
if (langIdent === undefined) { | if (langIdent === undefined) { | ||
langIdent = langIDValue; | langIdent = langIDValue; | ||
} | } else { | ||
// langIdent is in a specific format | // langIdent is in a specific format | ||
const langTemplate = {} | const langTemplate = {}; | ||
if (isSkill) { | if (isSkill) { | ||
langTemplate.SKILLID = this.getLocalID(parentNode[nodeKey].skillID); | langTemplate.SKILLID = this.getLocalID(parentNode[nodeKey].skillID); | ||
Line 1,299: | Line 1,705: | ||
langTemplate['ID' + idx] = this.getLocalID(val); | langTemplate['ID' + idx] = this.getLocalID(val); | ||
}); | }); | ||
} | } else { | ||
langTemplate.ID = langIDValue; | langTemplate.ID = langIDValue; | ||
} | } | ||
Line 1,311: | Line 1,716: | ||
if (langProp.keySpecial !== undefined) { | if (langProp.keySpecial !== undefined) { | ||
// Use a special method to determine the category key | // Use a special method to determine the category key | ||
switch(langProp.keySpecial) { | switch (langProp.keySpecial) { | ||
case 'shopChainKey': | case 'shopChainKey': | ||
langCategoryKey = shopChainPropKey(target, langPropID, 'category'); | langCategoryKey = shopChainPropKey(target, langPropID, 'category'); | ||
Line 1,325: | Line 1,730: | ||
if (this.debugMode) { | if (this.debugMode) { | ||
if (langString !== undefined) { | if (langString !== undefined) { | ||
console.debug('Set value of property ' + langPropID + '[' + num.toString() + '] for ' + langIdentFinal + ' in node ' + nodeName + ' to: ' + langString); | console.debug( | ||
} | 'Set value of property ' + | ||
langPropID + | |||
console.debug('No translation: property ' + langPropID + ' for ' + langIdentFinal + ' in node ' + nodeName); | '[' + | ||
num.toString() + | |||
'] for ' + | |||
langIdentFinal + | |||
' in node ' + | |||
nodeName + | |||
' to: ' + | |||
langString | |||
); | |||
} else { | |||
console.debug( | |||
'No translation: property ' + | |||
langPropID + | |||
' for ' + | |||
langIdentFinal + | |||
' in node ' + | |||
nodeName | |||
); | |||
} | } | ||
} | } | ||
}); | }); | ||
} | } else { | ||
let langString; | let langString; | ||
if (langProp.stringSpecial !== undefined) { | if (langProp.stringSpecial !== undefined) { | ||
// Use a custom function to determine the string | // Use a custom function to determine the string | ||
switch(langProp.stringSpecial) { | switch (langProp.stringSpecial) { | ||
case 'itemDesc': | case 'itemDesc': | ||
langString = itemDesc(target); | langString = itemDesc(target); | ||
break; | |||
case 'shopPurchaseDesc': | |||
langString = shopPurchaseDesc(target); | |||
break; | break; | ||
case 'passiveDesc': | case 'passiveDesc': | ||
Line 1,355: | Line 1,779: | ||
case 'tsWorshipStatueName': | case 'tsWorshipStatueName': | ||
langString = tsWorshipStatueName(target); | langString = tsWorshipStatueName(target); | ||
break; | |||
case 'attackSpellbooksName': | |||
langString = attackSpellbooksName(target); | |||
break; | |||
case 'attackSpellName': | |||
langString = attackSpellName(target); | |||
break; | break; | ||
} | } | ||
} | } else { | ||
langString = this.getLangString(langCategoryKey, langIdent); | langString = this.getLangString(langCategoryKey, langIdent); | ||
} | } | ||
Line 1,364: | Line 1,793: | ||
if (this.debugMode) { | if (this.debugMode) { | ||
if (langString !== undefined) { | if (langString !== undefined) { | ||
console.debug('Set value of property ' + langPropID + ' for ' + langIdent + ' in node ' + nodeName + ' to: ' + langString); | console.debug( | ||
} | 'Set value of property ' + | ||
langPropID + | |||
console.debug('No translation: property ' + langPropID + ' for ' + langIdent + ' in node ' + nodeName); | ' for ' + | ||
langIdent + | |||
' in node ' + | |||
nodeName + | |||
' to: ' + | |||
langString | |||
); | |||
} else { | |||
console.debug( | |||
'No translation: property ' + langPropID + ' for ' + langIdent + ' in node ' + nodeName | |||
); | |||
} | } | ||
} | } | ||
Line 1,381: | Line 1,820: | ||
} | } | ||
getLangString(key, identifier) { | getLangString(key, identifier) { | ||
if ( | let lookupVal = ''; | ||
if (key !== undefined) { | |||
lookupVal = key; | |||
} | } | ||
if (identifier !== undefined) { | |||
lookupVal += (lookupVal.length > 0 ? '_' : '') + identifier; | |||
} | } | ||
return this.customLocalizations[lookupVal] ?? loadedLangJson[lookupVal]; | |||
} | } | ||
getNamespacedID(namespace, ID) { | getNamespacedID(namespace, ID) { | ||
if (ID.indexOf(':') > 0) { | if (ID.indexOf(':') > 0) { | ||
return ID; | return ID; | ||
} | } else { | ||
return namespace + ':' + ID; | return namespace + ':' + ID; | ||
} | } | ||
} | } | ||
getLocalID(ID) { | getLocalID(ID) { | ||
if (ID.indexOf(':') > 0) { | if (ID !== undefined && ID.indexOf(':') > 0) { | ||
return ID.split(':').pop(); | return ID.split(':').pop(); | ||
} | } else { | ||
return ID; | return ID; | ||
} | } | ||
Line 1,406: | Line 1,845: | ||
} | } | ||
let wd = new Wiki; | let wd = new Wiki(); | ||
wd.printWikiData();</syntaxhighlight>}} | wd.printWikiData();</syntaxhighlight>}} |
Latest revision as of 00:06, 19 September 2024
The GameData module is the source of all game data which many other Lua modules rely upon. This module deals with the initial loading of the game data structure, and then enables other modules to access this both via a library of functions (preferred) and in its raw format.
The game data used by the wiki is currently at version V1.3.1 (?11964).
To generate game data, do the following:
- Navigate to https://melvoridle.com within your preferred web browser
- Select any character, the character that is chosen has no impact but you may consider creating a new one as a precaution - the below code is designed to execute without affecting the character, although this is not guaranteed
- Ensure mods are disabled such that the generated data excludes any modded content. If disabling mods, the game should be reloaded first before trying to generate game data
- Open the browser console/developer mode (usually by hitting the F12 key for most browsers)
- Within the browser console, enter the following code then hit enter. If successful, the game data should appear within the console
- Copy the game data & update Module:GameData/data, Module:GameData/data2, Module:GameData/data3 accordingly
Code |
---|
class Wiki {
constructor() {
this.debugMode = false;
this.prettyPrint = false;
this.customLocalizations = {
// Contains custom localization strings, to override any game provided localizations.
// To be used sparingly, for instances where 2+ objects of the same type
// (e.g. monsters) have the same name, as this isn't convenient to deal with in Lua
// TotH curse also named 'Madness'
MAGIC_ABYSSAL_NAME_Madness: 'Madness (ItA)',
// Stronghold boss monsters, where names overlap with normal monster variants
MONSTER_NAME_FierceDevilBoss: 'Fierce Devil (Stronghold)',
MONSTER_NAME_ElementalistBoss: 'Elementalist (Stronghold)',
MONSTER_NAME_PratTheGuardianOfSecretsBoss: 'Prat, the Guardian of Secrets (Stronghold)',
MONSTER_NAME_MysteriousFigurePhase1Stronghold: 'Mysterious Figure - Phase 1 (Stronghold)',
MONSTER_NAME_MysteriousFigurePhase2Stronghold: 'Mysterious Figure - Phase 2 (Stronghold)',
MONSTER_NAME_AhreniaStronghold: 'Ahrenia (Stronghold)'
};
this.namespaces = {
melvorD: {
displayName: 'Demo',
packFile: 'melvorDemo.json',
},
melvorF: {
displayName: 'Full Version',
packFile: 'melvorFull.json',
},
melvorTotH: {
displayName: 'Throne of the Herald',
packFile: 'melvorTotH.json',
},
melvorAoD: {
displayName: 'Atlas of Discovery',
packFile: 'melvorExpansion2.json',
},
melvorBirthday2023: {
displayName: 'Melvor Birthday 2023',
packFile: 'melvorBirthday2023.json',
},
melvorItA: {
displayName: 'Into the Abyss',
packFile: 'melvorItA.json',
},
};
this.registeredNamespaces = [];
// List of categories to be excluded from the generated game data.
// These serve no purpose for the wiki and so would otherwise bloat the data
this.excludedCategories = [
'pages',
'steamAchievements',
'tutorialStageOrder',
'tutorialStages'
];
// Check all required namespaces are registered, as there are still some bits of data extracted from in-game rather than the data packages
Object.keys(this.namespaces).forEach((nsID) => {
const nsTest = game.registeredNamespaces.getNamespace(nsID);
if (nsTest === undefined) {
throw new Error(
`Namespace ${nsID} (${this.namespaces[nsID].displayName}) is not registered - Ensure you are signed in and have the expansion.`
);
}
});
// The output data is now long enough that it exceeds the maximum allowed MediaWiki article
// length of 2,048KB. The below determines how the data should be separated over multiple
// pages (Module:GameData then combines the data into a single structure upon
// initialization).
this.maxPageBytes = 2 * 1024 ** 2; // 2048KB
this.printPages = [
{ includeCategories: '*', destination: 'Module:GameData/data' },
{
includeCategories: ['items'],
destination: 'Module:GameData/data2',
},
{
includeCategories: [
'itemUpgrades',
'itemSynergies',
'modifiers',
'shopPurchases',
'realms',
'damageTypes',
'combatTriangleSets',
'randomAbyssalGems',
'randomFragments',
'randomFiremakingOils',
'ancientRelics',
'attackSpells',
'attacks',
'combatPassives',
'monsters',
'bankSortOrder',
'combatEffects',
'combatEffectTemplates',
'combatEffectGroups'
],
destination: 'Module:GameData/data3',
},
];
this.packData = {};
this.gameData = {};
this.skillDataInit = {};
}
getDataPackURL(nsID) {
return 'https://' + location.hostname + '/assets/data/' + this.namespaces[nsID].packFile + '?' + DATA_VERSION.toString();
}
async getWikiData() {
if (!isLoaded) {
throw new Error('Game must be loaded into a character first');
}
for (const nsIdx in Object.keys(this.namespaces)) {
const ns = Object.keys(this.namespaces)[nsIdx];
const dataURL = this.getDataPackURL(ns);
console.log(`URL: ${dataURL}`);
const dataPackage = await this.getDataPackage(dataURL);
if (dataPackage.namespace === undefined) {
throw new Error(`Data package has no namespace: ${dataURL}`);
} else if (dataPackage.data === undefined) {
throw new Error(`Data package has no data: ${dataURL}`);
}
console.log(
`Obtained data for namespace ${dataPackage.namespace}, ${JSON.stringify(
dataPackage.data
).length.toLocaleString()} bytes`
);
this.processDataPackage(dataPackage);
console.log(`After transformation: ${JSON.stringify(dataPackage.data).length.toLocaleString()} bytes`);
}
// Process dependent data after all packages processed
console.log('Processing dependent data for all packages...');
this.processDependentData();
// All data packages should now be within this.gameData
}
getGameFileVersion() {
const fileDOM = document.querySelector('#sidebar ul.nav-main');
let fileVer = 'Unknown';
if (fileDOM !== null && fileDOM.dataset !== undefined) {
return fileDOM.dataset.fileVersion;
}
}
getGameVersion() {
return gameVersion;
}
getGameVersionText() {
return this.getGameVersion() + ' (' + this.getGameFileVersion() + ')';
}
getObjectByID(data, objectID, idKey = 'id') {
if (data !== undefined && objectID !== undefined) {
return data.find((obj) => obj[idKey] === objectID);
}
}
getCategoriesForPage(page) {
if (Array.isArray(page.includeCategories)) {
return page.includeCategories;
} else if (page.includeCategories === '*') {
// Special value, include all categories other than those included within
// other pages
return Object.keys(this.gameData).filter(
(cat) => !this.printPages.some((p) => Array.isArray(p.includeCategories) && p.includeCategories.includes(cat))
);
}
}
escapeQuotes(data) {
var newData = data.replace(/\\/g, '\\\\');
newData = newData.replace(/'/g, "\\'");
newData = newData.replace(/"/g, '\\"');
return newData;
}
formatJSONData(category, data) {
if (data === undefined) {
console.warn(`dataFormatter: Data for category ${category} is undefined`);
return '';
}
if (this.debugMode) {
console.debug('Formatting category data: ' + category);
}
if (category === 'skillData') {
return '"' + category + '":[' + data.map((x) => this.escapeQuotes(JSON.stringify(x))).join(",' ..\n'") + ']';
} else {
return '"' + category + '":' + this.escapeQuotes(JSON.stringify(data));
}
}
dataFullyLoaded() {
return Object.keys(this.packData).length >= Object.keys(this.namespaces).length;
}
printCategoryDataLength() {
if (!this.dataFullyLoaded()) {
throw new Error('Game data not loaded, use printWikiData first');
}
let dataLengths = [];
this.printPages.forEach((page) => {
const inclCat = this.getCategoriesForPage(page);
inclCat.forEach((cat) => {
dataLengths.push({
page: page.destination,
category: cat,
length: this.formatJSONData(cat, this.gameData[cat]).length,
});
});
});
console.table(dataLengths);
}
async printWikiData() {
if (!isLoaded) {
throw new Error('Game must be loaded into a character first');
}
if (!this.dataFullyLoaded()) {
// Need to retrieve game data first
const result = await this.getWikiData();
}
console.log('Printing data for game version ' + this.getGameVersionText());
this.printPages.forEach((page) => {
const inclCat = this.getCategoriesForPage(page);
let gameDataFiltered = {};
inclCat.forEach((cat) => (gameDataFiltered[cat] = this.gameData[cat]));
// Convert game data into a JSON string for export
let dataText;
if (this.prettyPrint) {
dataText = JSON.stringify(gameDataFiltered, undefined, '\t');
} else {
dataText = JSON.stringify(gameDataFiltered);
}
console.log(`For page "${page.destination}" (${dataText.length.toLocaleString()} bytes):`);
if (dataText.length > this.maxPageBytes) {
console.warn(
`Page "${page.destination}" exceeds max page size of ${(this.maxPageBytes / 1024).toLocaleString()}KB by ${(
dataText.length - this.maxPageBytes
).toLocaleString()} bytes. Consider amending the printPages configuration to move some data categories from this page onto other pages.`
);
}
console.log(dataText);
});
}
async getDataPackage(url) {
// Based largely on Game.fetchAndRegisterDataPackage()
const headers = new Headers();
headers.append('Content-Type', 'application/json');
return await fetch(url, {
method: 'GET',
headers,
}).then(function (response) {
if (!response.ok) {
throw new Error(`Couldn't fetch data package from URL: ${url}`);
}
return response.json();
});
}
processDataPackage(dataPackage) {
// Transforms the raw data from data packages in various ways, then
// consolidates into this.packData & this.gameData
const ns = dataPackage.namespace;
const packData = dataPackage.data;
this.transformDataPackage(dataPackage);
this.packData[dataPackage.namespace] = dataPackage;
this.registerDataPackage(dataPackage.namespace);
}
transformDataPackage(dataPackage) {
// Takes a raw data package and performs various manipulations
const ns = dataPackage.namespace;
const packData = dataPackage.data;
Object.keys(packData)
.filter((categoryName) => !this.excludedCategories.includes(categoryName))
.forEach((categoryName) => {
this.transformDataNode(ns, categoryName, packData, categoryName);
});
}
transformDataNode(ns, categoryName, parentNode, nodeKey) {
let dataNode = parentNode[nodeKey];
const transformedValue = this.transformProperty(categoryName, dataNode, nodeKey, ns);
if (transformedValue !== undefined) {
// A transformed value exists for this node
parentNode[nodeKey] = transformedValue;
dataNode = parentNode[nodeKey];
}
if (Array.isArray(dataNode)) {
// Recursive call to ensure all data is transformed, regardless of its depth
dataNode.forEach((entity, idx) => this.transformDataNode(ns, categoryName, dataNode, idx));
} else if (typeof dataNode === 'object' && dataNode !== null) {
// Iterate properties of object, checking if each should be deleted or transformed
Object.keys(dataNode).forEach((key) => {
// Check if property is to be deleted or not
if (this.isPropertyFiltered(categoryName, dataNode, key)) {
delete dataNode[key];
} else if (typeof dataNode[key] === 'object' && dataNode[key] !== null) {
// If an object (either an array or key/value store) is within the current
// object then we must traverse this too
this.transformDataNode(ns, categoryName, dataNode, key);
} else {
// Transform property, if a transformation is defined below
switch (key) {
case 'id':
// Add namespace to ID if it isn't already
const id = dataNode[key];
if (!Number.isInteger(id)) dataNode[key] = this.getNamespacedID(ns, dataNode[key]);
break;
}
}
});
}
// Apply localization, except for if this is skill data. That is handled separately below
if (categoryName !== 'skillData' && categoryName == nodeKey) {
this.langApply(parentNode, nodeKey, false);
}
// Special case for skillData so that certain values initialized when the various Skill
// classes are initialized may be added here also
if (categoryName === 'skillData' && dataNode.skillID !== undefined && dataNode.data !== undefined) {
// We are currently at the topmost level of a skill object
const gameSkill = game.skills.getObjectByID(dataNode.skillID);
// For every skill with mastery, add mastery checkpoint descriptions
if (
gameSkill instanceof SkillWithMastery &&
dataNode.data.masteryTokenID !== undefined &&
dataNode.data.masteryCheckpoints === undefined
) {
const localID = this.getLocalID(dataNode.skillID);
dataNode.data.baseMasteryPoolCap = gameSkill.baseMasteryPoolCap;
dataNode.data.masteryCheckpoints = [];
masteryCheckpoints.forEach((pct, idx) => {
dataNode.data.masteryCheckpoints[idx] = this.getLangString('MASTERY_CHECKPOINT', `${localID}_${idx}`);
});
}
if (!this.skillDataInit[dataNode.skillID]) {
if (gameSkill !== undefined) {
// Import other attributes varying by skill
let importKeys = [];
switch (dataNode.skillID) {
case 'melvorD:Mining':
importKeys = ['baseInterval', 'baseRockHP', 'passiveRegenInterval'];
dataNode.data.baseGemChance = 1;
dataNode.data.rockTypes = loadedLangJson.MINING_TYPE;
break;
case 'melvorItA:Harvesting':
importKeys = [
'baseInterval',
'baseVeinIntensity',
'passiveRegenInterval',
'uniqueProductChance',
'hpCheckpoints',
];
break;
case 'melvorD:Smithing':
case 'melvorD:Fletching':
case 'melvorD:Crafting':
case 'melvorD:Runecrafting':
case 'melvorD:Herblore':
importKeys = ['baseInterval'];
break;
case 'melvorD:Thieving':
importKeys = ['baseInterval', 'baseStunInterval', 'itemChance', 'baseAreaUniqueChance'];
break;
case 'melvorD:Summoning':
importKeys = ['baseInterval'];
const sumKeys = ['recipeAPCost', 'recipeGPCost', 'markLevels'];
sumKeys.forEach((k) => (dataNode.data[k] = Summoning[k]));
break;
case 'melvorD:Astrology':
// Astrology has a number of values stored outside of gameSkill
const astKeys = [
'standardModifierLevels',
'uniqueModifierLevels',
'abyssalModifierLevels',
'baseInterval',
];
astKeys.forEach((k) => (dataNode.data[k] = Astrology[k]));
break;
case 'melvorD:Township':
// Remap a number of keys from their in-game names
const townKeys = [
{ from: 'BASE_STORAGE', to: 'baseStorage' },
{ from: 'BASE_TAX_RATE', to: 'baseTaxRate' },
{ from: 'DECREASED_BUILDING_COST_CAP', to: 'decreasedBuildingCostCap' },
{ from: 'GP_PER_CITIZEN', to: 'gpPerCitizen' },
{ from: 'MAX_WORSHIP', to: 'maxWorship' },
{ from: 'MINIMUM_HEALTH', to: 'minimumHealth' },
{ from: 'populationForTier', to: 'populationForTier' },
{ from: 'TICK_LENGTH', to: 'tickLength' },
{ from: 'RARE_SEASON_CHANCE', to: 'rareSeasonChance' },
{ from: 'WORSHIP_CHANGE_COST', to: 'worshipChangeCost' },
{ from: 'WORSHIP_CHECKPOINTS', to: 'worshipCheckpoints' },
{ from: 'BASE_MAX_HEALTH', to: 'baseMaxHealth' },
{ from: 'abyssalTierRequirements', to: 'abyssalTierRequirements' },
{ from: 'BASE_SOUL_STORAGE', to: 'baseSoulStorage' },
];
townKeys.forEach((k) => (dataNode.data[k.to] = gameSkill[k.from]));
// Add task categories & localization of name
const taskCategories = Array.from(new Set(gameSkill.tasks.tasks.allObjects.map((t) => t.category)));
dataNode.data.taskCategories = taskCategories.map((category) => ({
id: category.id,
name: category.name,
}));
break;
}
if (importKeys.length > 0) {
importKeys.forEach((k) => (dataNode.data[k] = gameSkill[k]));
}
}
this.skillDataInit[dataNode.skillID] = true;
}
// Appy localization (skills)
this.langApply(parentNode, nodeKey, true);
}
}
registerPackData(packData) {
Object.keys(packData)
.filter((categoryName) => !this.excludedCategories.includes(categoryName))
.forEach((categoryName) => {
let categoryData = packData[categoryName];
// Some data is adjusted before combining - do this here
if (['combatAreas', 'dungeons', 'slayerAreas', 'abyssDepths', 'strongholds'].includes(categoryName)) {
// Add area type to each area object
const areaTypes = {
combatAreas: 'combatArea',
dungeons: 'dungeon',
slayerAreas: 'slayerArea',
strongholds: 'stronghold',
abyssDepths: 'abyssDepth',
};
const areaType = areaTypes[categoryName];
const newData = structuredClone(categoryData);
newData.forEach((x) => (x.type = areaType));
categoryData = newData;
}
else if (categoryName === 'golbinRaid') {
}
// Data must be pushed into the consoldiated data, rules for vary
// depending on the category in question
switch (categoryName) {
case 'realms':
case 'attackSpellbooks':
case 'damageTypes':
case 'equipmentSlots':
case 'combatAreaCategories':
case 'combatEffects':
case 'combatEffectGroups':
case 'combatEffectTables':
case 'combatEffectTemplates':
case 'combatTriangleSets':
case 'masterPoolBonuses':
case 'masteryLevelUnlocks':
case 'masteryLevelBonuses':
case 'masterPoolBonuses':
case 'ancientRelics':
case 'attackSpells':
case 'attackStyles':
case 'attacks':
case 'auroraSpells':
case 'combatAreas':
case 'combatEvents':
case 'combatPassives':
case 'curseSpells':
case 'dungeons':
case 'strongholds':
case 'abyssDepths':
case 'gamemodes':
case 'itemEffects':
case 'itemSynergies':
case 'itemUpgrades':
case 'itmMonsters':
case 'items':
case 'lore':
case 'modifiers':
case 'monsters':
case 'pages':
case 'pets':
case 'prayers':
case 'randomGems':
case 'randomSuperiorGems':
case 'randomAbyssalGems':
case 'randomFragments':
case 'randomFiremakingOils':
case 'shopCategories':
case 'shopPurchases':
case 'shopUpgradeChains':
case 'skillLevelCapIncreases':
case 'slayerAreas':
case 'slayerTaskCategories':
case 'steamAchievements':
case 'tutorialStages':
case 'spiderLairMonsters':
// Plain old push to the end of the array
if (this.gameData[categoryName] === undefined) {
// Category doesn't exist yet in consolidated data, so create it
this.gameData[categoryName] = categoryData;
} else {
this.gameData[categoryName].push(...categoryData);
}
break;
case 'ancientRelicsDisplayOrder':
case 'bankSortOrder':
case 'combatAreaCategoryOrder':
case 'combatAreaDisplayOrder':
case 'dungeonDisplayOrder':
case 'shopCategoryOrder':
case 'shopDisplayOrder':
case 'skillTreesDisplayOrder':
case 'slayerAreaDisplayOrder':
case 'tutorialStageOrder':
// Elements are inserted at a particular index, controlled by rules
// specified within the data package
this.gameData[categoryName] = this.combineOrderedData(this.gameData[categoryName], categoryData);
break;
case 'golbinRaid':
// Properties contain unordered arrays that need to be combined
if (this.gameData[categoryName] === undefined) {
this.gameData[categoryName] = categoryData;
this.gameData.golbinRaid.possibleModifiers = RaidManager.possibleModifiers;
} else {
Object.keys(categoryData).forEach((dataKey) => {
if (
this.gameData[categoryName][dataKey] === undefined ||
!Array.isArray(this.gameData[categoryName][dataKey])
) {
// Property is undefined or isn't an array
this.gameData[categoryName][dataKey] = categoryData[dataKey];
} else {
// Property is an array
this.gameData[categoryName][dataKey].push(...categoryData[dataKey]);
}
});
}
break;
case 'skillData':
// Contains nested objects
if (this.gameData[categoryName] === undefined) {
this.gameData[categoryName] = [];
}
// Find the appropriate skill object and combine properties with that
categoryData.forEach((skillData) => {
var skillIdx = this.gameData[categoryName].findIndex((skill) => skill.skillID === skillData.skillID);
if (skillIdx === -1) {
// Initialize skill
const initData = structuredClone(skillData);
initData.data = {};
this.gameData[categoryName].push(initData);
skillIdx = this.gameData[categoryName].findIndex((skill) => skill.skillID === skillData.skillID);
}
const skillObj = this.gameData[categoryName][skillIdx].data;
Object.keys(skillData.data).forEach((dataKey) => {
// Special case for Township item conversions
if (
skillObj[dataKey] !== undefined &&
skillData.skillID === 'melvorD:Township' &&
dataKey === 'itemConversions'
) {
Object.keys(skillData.data[dataKey]).forEach((convKey) => {
skillData.data[dataKey][convKey].forEach((resource) => {
// Find the resource if it already exists within the combined data
const resourceIdx = skillObj[dataKey][convKey].findIndex(
(res) => res.resourceID === resource.resourceID
);
if (resourceIdx === -1) {
skillObj[dataKey][convKey].push(resource);
} else {
skillObj[dataKey][convKey][resourceIdx].items.push(...resource.items);
}
});
});
} else if (
Array.isArray(skillData.data[dataKey]) &&
skillData.data[dataKey].length > 0 &&
skillData.data[dataKey][0].insertAt !== undefined
) {
// Data is ordered, special handling applies
skillObj[dataKey] = this.combineOrderedData(skillObj[dataKey], skillData.data[dataKey]);
} else if (skillObj[dataKey] === undefined || !Array.isArray(skillObj[dataKey])) {
// Property is undefined or isn't an array
skillObj[dataKey] = skillData.data[dataKey];
} else {
// Property is an array
skillObj[dataKey].push(...skillData.data[dataKey]);
}
});
});
break;
default:
console.warn(`Skipping unknown category while registering data package: ${categoryName}`);
break;
}
});
}
registerDataPackage(namespace) {
// Consolidates the data package identified by namespace with existing data within
// this.gameData
const packData = this.packData[namespace].data;
if (packData === undefined) {
throw new Error(`Couldn't find data for package ${namespace}`);
}
// Add data within the game but outside of data packs
this.registerNonPackData();
// Consolidate data
this.registerPackData(packData);
// If the data package contains modifications, apply these also
const modificationData = this.packData[namespace].modifications;
if (modificationData !== undefined) {
this.applyDataModifications(modificationData);
}
// Dependent data is handled later, once all packages have been registered
if (!this.registeredNamespaces.includes(namespace)) {
this.registeredNamespaces.push(namespace);
}
}
processDependentData() {
Object.entries(this.packData)
.forEach(([namespace, packData]) => {
if (packData.dependentData !== undefined) {
packData.dependentData.forEach((depDataForNS) => {
const depNS = depDataForNS.namespace;
if (!this.registeredNamespaces.includes(depNS)) {
console.warn(
`Could not apply dependent data from package ${namespace}: Data depends on namespace ${depNS}, which has not been registered`
);
}
else {
console.log(`Attempting to apply dependent data for ${depNS} from package ${namespace}`);
if (depDataForNS.data !== undefined) {
this.registerPackData(depDataForNS.data)
}
if (depDataForNS.modifications !== undefined) {
this.applyDataModifications(depDataForNS.modifications);
}
}
});
}
});
}
getDataToModify(modCat) {
switch (modCat) {
case 'combatAreaCategories':
case 'damageTypes':
case 'dungeons':
case 'equipmentSlots':
case 'gamemodes':
case 'items':
case 'modifiers':
case 'pets':
case 'shopUpgradeChains':
case 'shopPurchases':
case 'skillData':
case 'slayerAreas':
return this.gameData[modCat];
case 'cookingCategories':
const cookingSkill = this.getObjectByID(this.gameData.skillData, 'melvorD:Cooking', 'skillID');
return cookingSkill.data.categories;
case 'fletchingRecipes':
const fletchingSkill = this.getObjectByID(this.gameData.skillData, 'melvorD:Fletching', 'skillID');
return fletchingSkill.data.recipes;
}
return undefined;
}
applyModifierModifications(objToModify, adjustments) {
if (objToModify.modifiers === undefined) {
objToModify.modifiers = {};
}
Object.keys(adjustments)
.forEach((adjType) => {
if (adjType === 'add') {
Object.entries(adjustments[adjType])
.forEach(([chgKey, chgVal]) => {
if (objToModify.modifiers[chgKey] === undefined) {
objToModify.modifiers[chgKey] = chgVal;
}
else if (Array.isArray(chgVal)) {
objToModify.modifiers[chgKey].push(...chgVal);
}
else {
objToModify.modifiers[chgKey] += chgVal;
}
});
}
else {
console.warn(
`Could not apply data modification: Unhandled modifier adjustment "${adjType}"`
);
}
}
);
}
applyAddRemoveModifications(objToModify, adjustments, modifyKey) {
if (adjustments.remove !== undefined && Array.isArray(objToModify[modifyKey])) {
// adjustments.remove is an array of requirement types to be removed
let i = 0;
while (i < objToModify[modifyKey].length) {
if (adjustments.remove.includes(objToModify[modifyKey][i].type)) {
objToModify[modifyKey].splice(i, 1);
}
else {
i++;
}
}
}
if (adjustments.add !== undefined) {
if (objToModify[modifyKey] === undefined) {
objToModify[modifyKey] = adjustments.add;
}
else {
objToModify[modifyKey].push(...adjustments.add);
}
}
}
applyGamemodeSpecificModifications(objToModify, adjustments, newProperty) {
const gamemodeID = adjustments.gamemodeID;
if (objToModify.gamemodeOverrides === undefined) {
objToModify.gamemodeOverrides = [];
}
let gamemodeEntryToModify = this.getObjectByID(objToModify.gamemodeOverrides, gamemodeID, 'gamemodeID');
if (gamemodeEntryToModify === undefined) {
// Initialize gamemode overrides
objToModify.gamemodeOverrides.push({
gamemodeID: gamemodeID
});
gamemodeEntryToModify = this.getObjectByID(objToModify.gamemodeOverrides, gamemodeID, 'gamemodeID');
}
if (gamemodeEntryToModify[newProperty] === undefined) {
gamemodeEntryToModify[newProperty] = structuredClone(objToModify[newProperty]) ?? {};
}
this.applyAddRemoveModifications(gamemodeEntryToModify, adjustments, newProperty);
}
applyDataModifications(modData) {
const modDataKeys = Object.keys(modData).filter((modCatID) => !this.excludedCategories.includes(modCatID));
for (const modCatID in modDataKeys) {
const modCat = modDataKeys[modCatID];
const catData = modData[modCat];
const dataToModify = this.getDataToModify(modCat);
const modObjIDKey = (modCat === 'skillData' ? 'skillID' : 'id');
if (dataToModify === undefined) {
console.warn(
`Could not apply data modification for category "${modCat}": Unable to retrieve category data to be modified`
);
}
else {
catData.forEach((modItem) => {
const modObjID = modItem[modObjIDKey];
if (modObjID === undefined) {
console.warn(
`Could not apply data modification for category "${modCat}": ID of object to be modified not found`
);
} else {
const objToModify = this.getObjectByID(dataToModify, modObjID, modObjIDKey);
if (objToModify === undefined) {
console.warn(
`Could not apply data modification: Object with ID "${modObjID}" not found for ctaegory "${modCat}"`
);
}
else {
switch (modCat) {
case 'combatAreaCategories':
// The 'areas' property of elements within the category data are ordered data
objToModify.areas = this.combineOrderedData(objToModify.areas, modItem.areas.add);
break;
case 'damageTypes':
Object.entries(modItem)
.filter(([k, v]) => k !== 'id')
.forEach(([k, v]) => {
if (typeof v === 'object' && (v.add !== undefined || v.remove !== undefined)) {
this.applyAddRemoveModifications(objToModify, v, k);
}
else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
});
break;
case 'gamemodes':
Object.entries(modItem)
.filter(([k, v]) => k !== 'id')
.forEach(([k, v]) => {
if (typeof v === 'object' && (v.add !== undefined || v.remove !== undefined)) {
this.applyAddRemoveModifications(objToModify, v, k);
}
else if (['abyssalLevelCapCost', 'post99RollConversion'].includes(k)) {
objToModify[k] = v;
}
else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
});
break;
case 'shopPurchases':
case 'shopUpgradeChains':
// Modify the root upgrade ID of shop upgrade chains, and modify attributes of shop purchases
const overrideKeys = {
purchaseRequirements: {
sourceKey: 'newRequirements', // Key that holds the data in the data package
destKey: 'purchaseRequirementsOverrides', // Key to insert into within this.gameData
subKey: 'requirements', // Sub-key containing the override data
},
cost: {
sourceKey: 'newCosts',
destKey: 'costOverrides',
subKey: 'cost',
},
};
Object.keys(modItem)
.filter((k) => k !== 'id')
.forEach((k) => {
const overrideKey = overrideKeys[k];
if (overrideKey !== undefined) {
// Is an override specific to a gamemode, do not replace
// the key's existing data
const destKey = overrideKey.destKey;
if (objToModify[destKey] === undefined) {
objToModify[destKey] = [];
}
modItem[k].forEach((gamemodeOverride) => {
var newData = {};
newData.gamemodeID = gamemodeOverride.gamemodeID;
newData[overrideKey.subKey] = gamemodeOverride[overrideKey.sourceKey];
objToModify[destKey].push(newData);
});
} else {
objToModify[k] = modItem[k];
}
});
break;
case 'cookingCategories':
// Append to the list of shop upgrade IDs for cooking utilities/categories
case 'fletchingRecipes':
// Append to alternativeCosts property of recipes (e.g. Arrow shafts)
Object.keys(modItem)
.filter((k) => k !== 'id')
.forEach((k) => {
if ((k === 'shopUpgradeIDs') || (k === 'alternativeCosts')) {
if (objToModify[k] === undefined) {
objToModify[k] = modItem[k];
} else {
objToModify[k].push(...modItem[k]);
}
} else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
});
break;
case 'skillData':
Object.entries(modItem.data)
.forEach(([skillProp, propModData]) => {
propModData.forEach((subModItem) => {
const subObjToModify = this.getObjectByID(objToModify.data[skillProp], subModItem.id);
if (subObjToModify === undefined) {
console.warn(`Couldn't find skill object with ID ${subModItem.id} to modify. Property ${skillProp} in skill ID ${objToModify.skillID}`);
}
else {
Object.entries(subModItem)
.forEach(([subProp, subData]) => {
if (subProp === 'modifiers') {
this.applyModifierModifications(subObjToModify, subData);
}
else if (subProp !== 'id') {
this.applyAddRemoveModifications(subObjToModify, subData, subProp);
}
});
}
});
});
break;
case 'dungeons':
// Add gamemode specific data to dungeons
Object.keys(modItem)
.filter((k) => k !== 'id')
.forEach((k) => {
if (k === 'gamemodeRewardItemIDs') {
// Add gamemode specific item rewards to dungeon data
const itemRules = modItem[k];
Object.keys(itemRules).forEach((ruleKey) => {
if (ruleKey === 'add') {
itemRules[ruleKey].forEach((itemDef) => {
const modToApply = {
gamemodeID: itemDef.gamemodeID,
add: itemDef.rewardItemIDs
}
this.applyGamemodeSpecificModifications(objToModify, modToApply, 'rewardItemIDs');
});
} else {
console.warn(
`Could not apply data modification: Unknown rule for gamemode item rewards: "${ruleKey}", object "${modObjID}"`
);
}
});
} else if (k === 'gamemodeEntryRequirements') {
// Add or remove gamemode specific entry requirements to dungeon data
this.applyGamemodeSpecificModifications(objToModify, modItem[k], 'entryRequirements');
} else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
});
break;
case 'modifiers':
// Add modifier aliases to existing mod scopes
if (objToModify.allowedScopes === undefined) {
console.warn(`Could not apply data modification: Modifier with ID ${modObjID} not found or modifier has no scopes`);
} else {
modItem.allowedScopes.forEach((srcScope) => {
// Find scope within modifier objToModify with matching scopes definition
const srcScopeKeys = Object.keys(srcScope.scopes);
objToModify.allowedScopes.forEach((destScope) => {
const destScopeKeys = Object.keys(destScope.scopes);
const scopeMatch = (
srcScopeKeys.length === destScopeKeys.length
&& srcScopeKeys.every((k) => destScope.scopes[k] !== undefined && srcScope.scopes[k] == destScope.scopes[k])
);
if (scopeMatch) {
// Scopes match - add aliases to modifier allowedScope definition
const aliasKeys = ['posAliases', 'negAliases'];
aliasKeys.forEach((aliasKey) => {
if (srcScope[aliasKey] !== undefined) {
if (destScope[aliasKey] === undefined) {
destScope[aliasKey] = [];
}
destScope[aliasKey].push(...srcScope[aliasKey]);
}
});
}
});
});
}
break;
case 'items':
Object.keys(modItem)
.filter((k) => k !== 'id')
.forEach((k) => {
if (k === 'modifiers') {
this.applyModifierModifications(objToModify, modItem[k]);
}
else if (k === 'consumesOn') {
Object.keys(modItem[k])
.forEach((adjType) => {
if (adjType === 'add') {
if (objToModify[k] === undefined) {
objToModify[k] = modItem[k][adjType];
}
else {
objToModify[k].push(...modItem[k][adjType]);
}
}
else {
console.warn(
`Could not apply data modification: Unhandled adjustment type "${adjType}" for category "${modCat}", object "${modObjID}, property ${k}"`
);
}
});
}
else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
}
);
break;
case 'pets':
Object.keys(modItem)
.filter((k) => k !== 'id')
.forEach((k) => {
if (k === 'modifiers') {
this.applyModifierModifications(objToModify, modItem[k]);
}
else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
}
);
break;
case 'equipmentSlots':
Object.keys(modItem)
.filter((k) => k !== 'id')
.forEach((k) => {
if (k === 'requirements') {
this.applyAddRemoveModifications(objToModify, modItem[k], 'requirements');
}
else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
}
);
break;
case 'slayerAreas':
Object.keys(modItem)
.filter((k) => k !== 'id')
.forEach((k) => {
if (k === 'gamemodeEntryRequirements') {
this.applyGamemodeSpecificModifications(objToModify, modItem[k], 'entryRequirements');
}
else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
}
);
break;
default:
console.warn(
`Could not apply data modification: Unhandled category "${modCat}"`
);
}
}
}
});
}
}
}
registerNonPackData() {
// Some data resides outside of packages. Add any such data to this.gameData within this function
// Metadata for data/file version
if (this.gameData._dataVersion === undefined) {
this.gameData._dataVersion = ({
gameVersion: this.getGameVersion().substring(1),
fileVersion: this.getGameFileVersion().substring(1)
});
}
// Namespaces
if (this.gameData.namespaces === undefined) {
const nsData = [];
game.registeredNamespaces.forEach((ns) => {
if (ns.isModded) {
throw new Error(
`Modded namespace '${ns.displayName}' found, all mods must be disabled before game data can be generated`
);
} else {
nsData.push(ns);
}
});
this.gameData.namespaces = nsData;
}
if (this.gameData.currencies === undefined) {
this.gameData.currencies = game.currencies.allObjects.map((c) => ({
id: c.id,
name: c.name,
type: c.type
}));
}
// Melvor realm exists outside of data packages
if (this.gameData.realms === undefined) {
this.gameData.realms = game.realms
.filter((r) => r.id === 'melvorD:Melvor')
.map((r) => ({
id: r.id,
name: r.name,
unlockRequirements: r.unlockRequirements
}));
}
// Normal damage type exists outside of data packages
if (this.gameData.damageTypes === undefined) {
this.gameData.damageTypes = game.damageTypes
.filter((d) => d.id === 'melvorD:Normal')
.map((d) => ({
id: d.id,
name: d.name,
resistanceCap: d._resistanceCap,
resistanceName: d.resistanceName
}));
}
if (this.gameData.combatAreaDifficulties === undefined) {
this.gameData.combatAreaDifficulties = CombatAreaMenuElement.difficulty.map((i) => i.name);
}
if (this.gameData.attackTypes === undefined) {
this.gameData.attackTypes = AttackTypeID;
}
}
combineOrderedData(existingData, newData) {
// Elements are inserted at a particular index, controlled by rules
// specified within the data package
var resultData = undefined;
if (existingData === undefined) {
resultData = [];
} else {
resultData = structuredClone(existingData);
}
newData.forEach((orderData) => {
switch (orderData.insertAt) {
case 'Start':
resultData.splice(0, 0, ...orderData.ids);
break;
case 'End':
resultData.push(...orderData.ids);
break;
case 'Before':
const beforeIdx = resultData.findIndex((item) => item === orderData.beforeID);
if (beforeIdx === -1) {
throw new Error(`Couldn't insert before: Item ${orderData.beforeID} is not in the array.`);
}
resultData.splice(beforeIdx, 0, ...orderData.ids);
break;
case 'After':
const afterIdx = resultData.findIndex((item) => item === orderData.afterID);
if (afterIdx === -1) {
throw new Error(`Couldn't insert after: Item ${orderData.afterID} is not in the array.`);
}
resultData.splice(afterIdx + 1, 0, ...orderData.ids);
break;
}
});
return resultData;
}
// Determines if properties of entities are to be removed, as they are unused in the wiki
// and would otherwise bloat the data.
// Returns true if the property is to be removed, false if it is to be retained
isPropertyFiltered(entityType, entity, propertyName) {
switch (propertyName) {
case 'media':
case 'altMedia':
case 'markMedia':
case 'icon':
case 'barStyle': // See: melvorD:Compost
case 'buttonStyle':
case 'descriptionGenerator':
case 'containerID':
case 'headerBgClass':
case 'textClass':
case 'btnClass':
return true;
case 'golbinRaidExclusive':
case 'ignoreCompletion':
case 'obtainFromItemLog':
// Property is boolean & isn't of interest when false
return !entity[propertyName];
case 'validSlots':
case 'occupiesSlots':
case 'equipRequirements':
case 'equipmentStats':
// Property is an array & isn't of interest when zero elements in length
return entity[propertyName].length === 0;
case 'tier':
if (entityType === 'items') {
return entity.tier === 'none';
} else {
return false;
}
default:
// Always retain property
return false;
}
}
// Specifies rules for transforming values of entity properties.
// Returns undefined if the property has no transformation
transformProperty(entityType, entity, propertyName, namespace) {
switch (propertyName) {
case 'langHint':
case 'langCustomDescription':
return this.getLangString(entity.category, this.getLocalID(entity.id));
case 'equipmentStats':
const newStats = {};
entity.forEach((stat) => {
let statKey = stat.key;
if (stat.damageType !== undefined) {
statKey += this.getLocalID(stat.damageType);
}
if (newStats[statKey] === undefined) {
newStats[statKey] = stat.value;
} else {
newStats[statKey] += stat.value;
}
});
return newStats;
case 'altSpells':
if (entityType !== 'skillData') {
return undefined;
} else {
const newData = structuredClone(entity);
newData.forEach((i) => {
i.spellBook = 'altMagic';
});
return newData;
}
default:
return undefined;
}
}
langApply(parentNode, nodeKey, isSkill) {
const nodeName = isSkill ? parentNode[nodeKey].skillID : nodeKey;
const altMagicDescIDKey = function (data) {
// Accepts an Alt. Magic spell object, returns the ID format for that spell
// Using a function for this as some spells (e.g. Superheat) have bespoke logic
if (data.specialCost !== undefined && data.specialCost.type !== undefined) {
if (data.id.includes('HolyInvocation')) {
return 'HOLY_INVOCATION';
}
switch (data.specialCost.type) {
case 'BarIngredientsWithCoal':
return 'SUPERHEAT';
case 'BarIngredientsWithoutCoal':
return 'SUPERHEAT_NO_COAL';
case 'AnyItem':
if (data.produces !== undefined && data.produces === 'GP') {
return 'ITEM_ALCHEMY';
}
break;
}
}
return 'ALTMAGIC_DESC_{ID}';
};
const shopChainPropKey = (data, dataKey, propName) => {
// Accepts an upgrade chain data object & key of the property being localized
const propToLang = {
chainName: 'chainNameLang',
defaultDescription: 'descriptionLang',
defaultName: 'defaultNameLang',
};
const langPropName = propToLang[dataKey];
if (langPropName !== undefined) {
const langProp = data[langPropName];
if (langProp !== undefined) {
return langProp[propName];
}
}
};
const itemDesc = (data) => {
const item = game.items.getObjectByID(data.id);
if (item !== undefined && item.hasDescription) {
return item.description;
} else return '';
};
const shopPurchaseDesc = (data) => {
const purchase = game.shop.purchases.getObjectByID(data.id);
if (purchase !== undefined) {
// Logic taken from description method of ShopPurchase class & slightly modified
// to avoid retrieving an item's modified description, which can include HTML
let desc = '';
if (purchase._customDescription !== undefined) {
if (purchase.isModded) {
return purchase._customDescription;
}
else {
return getLangString(`SHOP_DESCRIPTION_${ purchase.localID }`);
}
}
if (purchase.contains.itemCharges !== undefined) {
return purchase.contains.itemCharges.item.description;
}
if (purchase.contains.items.length === 1) {
return purchase.contains.items[0].item.description; // Was modifiedDescription
}
if (purchase.contains.pet !== undefined) {
return purchase.contains.pet.description;
}
if (purchase.contains.stats !== undefined) {
desc = purchase.contains.stats.describePlain();
}
if (purchase.hasDisabledModifier) {
desc += getLangString('MENU_TEXT_CONTAINS_DISABLED_MODIFIER');
}
return desc;
} else return '';
};
const relicDesc = (data) => {
const relic = game.ancientRelics.getObjectByID(data.id);
if (relic !== undefined) {
return relic.name;
}
};
const passiveDesc = (data) => {
const passive = game.combatPassives.getObjectByID(data.id);
if (passive !== undefined) {
return passive.description;
}
};
const spAttDesc = (data) => {
const spAtt = game.specialAttacks.getObjectByID(data.id);
if (spAtt !== undefined) {
return spAtt.description;
}
};
const tsWorshipName = (data) => {
const worship = game.township.worships.getObjectByID(data.id);
if (worship !== undefined) {
return worship.name;
}
};
const tsWorshipStatueName = (data) => {
const worship = game.township.worships.getObjectByID(data.id);
if (worship !== undefined) {
return worship.statueName;
}
};
const attackSpellbooksName = (data) => {
const book = game.attackSpellbooks.getObjectByID(data.id);
if (book !== undefined) {
return book.name;
}
};
const attackSpellName = (data) => {
const spell = game.attackSpells.getObjectByID(data.id);
if (spell !== undefined) {
return this.getLangString(`${ spell.spellbook.spellNameLangPrefix }${ spell.localID }`);
}
};
const hasNoLangData = [
// Categories that contain no localized text. Supresses warnings about no lang data
'ancientRelicsDisplayOrder',
'bankSortOrder',
'combatAreaDisplayOrder',
'combatAreaCategoryOrder',
'combatEffectTables',
'combatEffectTemplates',
'combatEvents',
'dungeonDisplayOrder',
'golbinRaid',
'itemEffects',
'itemSynergies',
'itemUpgrades',
'itmMonsters',
'modifiers', // TODO Does have lang data, supressing warning for now
'randomAbyssalGems',
'randomFiremakingOils',
'randomFragments',
'randomGems',
'randomSuperiorGems',
'slayerAreaDisplayOrder',
'slayerTaskCategories', // TODO Does have lang data, supressing warning for now
'shopCategoryOrder',
'shopDisplayOrder',
'skillLevelCapIncreases',
'skillTreesDisplayOrder',
'spiderLairMonsters',
'stackingEffects',
];
const langKeys = {
realms: {
name: { key: 'REALM', idFormat: 'NAME_{ID}' },
},
damageTypes: {
name: { idFormat: 'DAMAGE_TYPE_{ID}' },
},
combatTriangleSets: {
name: { key: 'COMBAT_TRIANGLE_NAME', idFormat: 'NAME_{ID}' },
},
attackSpellbooks: {
name: { stringSpecial: 'attackSpellbooksName' },
},
attackSpells: {
name: { stringSpecial: 'attackSpellName' },
},
ancientRelics: {
name: { stringSpecial: 'relicDesc' },
},
attackStyles: {
name: { key: 'COMBAT_MISC', idFormat: 'ATTACK_STYLE_NAME_{ID}' },
},
attacks: {
name: { key: 'SPECIAL_ATTACK_NAME' },
description: { stringSpecial: 'spAttDesc' },
},
auroraSpells: {
name: { key: 'MAGIC', idFormat: 'AURORA_NAME_{ID}' },
},
combatAreaCategories: {
name: { key: 'COMBAT_AREA_CATEGORY' }
},
combatAreas: {
name: { key: 'COMBAT_AREA', idFormat: 'NAME_{ID}' },
},
combatEffectGroups: {
name: { idKey: 'nameLang' }
},
combatEffects: {
name: { idKey: 'nameLang' }
},
combatPassives: {
name: { key: 'PASSIVES', idFormat: 'NAME_{ID}' },
customDescription: { stringSpecial: 'passiveDesc' },
//customDescription: { key: 'PASSIVES', idFormat: 'DESC_{ID}' }
},
curseSpells: {
name: { key: 'MAGIC', idFormat: 'CURSE_NAME_{ID}' },
},
dungeons: {
name: { key: 'DUNGEON', idFormat: 'NAME_{ID}' },
},
abyssDepths: {
name: { key: 'THE_ABYSS', idFormat: 'NAME_{ID}' },
},
strongholds: {
name: { idFormat: 'STRONGHOLD_NAME_{ID}' },
},
equipmentSlots: {
emptyName: { idFormat: 'EQUIP_SLOT_{ID}' }
},
gamemodes: {
name: { key: 'GAMEMODES', idFormat: 'GAMEMODE_NAME_{ID}' },
description: { key: 'GAMEMODES', idFormat: 'GAMEMODE_DESC_{ID}' },
// Gamemodes have an array of rules
rules: { key: 'GAMEMODES', idFormat: 'GAMEMODE_RULES_{ID}_{NUM}' },
},
items: {
name: { key: 'ITEM_NAME' },
customDescription: { stringSpecial: 'itemDesc', onlyIfExists: false },
},
lore: {
title: { key: 'LORE', idFormat: 'TITLE_{ID}' },
},
monsters: {
name: { key: 'MONSTER_NAME' },
description: { key: 'MONSTER_DESCRIPTION' },
},
pets: {
name: { key: 'PET_NAME' },
hint: { idKey: 'langHint' }
},
prayers: {
name: { key: 'PRAYER', idFormat: 'PRAYER_NAME_{ID}' },
},
shopCategories: {
name: { key: 'SHOP_CAT' },
},
shopPurchases: {
customName: { key: 'SHOP_NAME', onlyIfExists: true },
customDescription: { stringSpecial: 'shopPurchaseDesc', onlyIfExists: false },
},
shopUpgradeChains: {
chainName: { keySpecial: 'shopChainKey', idSpecial: 'shopChainID' },
defaultDescription: { keySpecial: 'shopChainKey', idSpecial: 'shopChainID' },
defaultName: { keySpecial: 'shopChainKey', idSpecial: 'shopChainID' },
},
slayerAreas: {
name: { key: 'SLAYER_AREA', idFormat: 'NAME_{ID}' },
areaEffectDescription: { key: 'SLAYER_AREA', idFormat: 'EFFECT_{ID}' },
},
skillData: {
// Each skill is nested within this, so follow much the same structure
// Keys here are each skill's local ID
_common: {
// Special entry, contains lang definitions which are the same
// for all skills
_root: {
name: { key: 'SKILL_NAME', idFormat: '{SKILLID}' },
},
customMilestones: {
name: { key: 'MILESTONES', idKey: 'milestoneID' },
},
masteryLevelUnlocks: {
description: { key: 'MASTERY_BONUS', idKey: 'descriptionID', idFormat: '{SKILLID}_{ID}' },
},
},
Archaeology: {
digSites: {
name: { key: 'POI_NAME_Melvor' },
},
// TODO Tool names
},
Agility: {
elitePillars: {
name: { key: 'AGILITY', idFormat: 'PILLAR_NAME_{ID}' },
},
obstacles: {
name: { key: 'AGILITY', idFormat: 'OBSTACLE_NAME_{ID}' },
},
pillars: {
name: { key: 'AGILITY', idFormat: 'PILLAR_NAME_{ID}' },
},
},
Astrology: {
recipes: {
name: { key: 'ASTROLOGY', idFormat: 'NAME_{ID}' },
},
},
Cartography: {
mapPortals: { _handler: 'mapPortals' },
travelEvents: {
description: { key: 'TRAVEL_EVENT' },
},
worldMaps: { _handler: 'cartoMaps' },
//name: { key: 'WORLD_MAP_NAME' },
//pointsOfInterest: { _handler: 'mapPOI' }
//name: { key: 'POI_NAME', idFormat: '{MAPID}_{ID}' },
//description: { key: 'POI_DESCRIPTION', idFormat: '{MAPID}_{ID}' }
},
Cooking: {
categories: {
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'}
}
},
Crafting: {
categories: {
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'}
}
},
Farming: {
categories: {
description: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_description' },
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'},
seedNotice: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_seedNotice' },
singularName: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_singular' },
},
},
Fletching: {
categories: {
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'}
}
},
Fishing: {
areas: {
name: { key: 'FISHING', idFormat: 'AREA_NAME_{ID}' },
},
},
Herblore: {
categories: {
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'}
},
recipes: {
name: { key: 'POTION_NAME' },
},
},
Magic: {
altSpells: {
name: { key: 'MAGIC', idFormat: 'ALTMAGIC_NAME_{ID}' },
description: { key: 'MAGIC', idSpecial: 'altMagicDesc' },
},
},
Mining: {
categories: {
name: { idFormat: 'MINING_TYPE_{ID}' }
},
rockData: {
name: { key: 'ORE_NAME' },
},
},
Runecrafting: {
categories: {
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'}
}
},
Summoning: {
categories: {
name: { idFormat: 'SKILL_CATEGORY_{SKILLID}_{ID}'}
},
synergies: {
customDescription: {
key: 'SUMMONING_SYNERGY',
idKey: 'summonIDs',
idFormat: 'DESC_{ID0}_{ID1}',
onlyIfExists: true,
},
},
},
Thieving: {
areas: {
name: { key: 'THIEVING', idFormat: 'AREA_NAME_{ID}' },
},
npcs: {
name: { key: 'THIEVING', idFormat: 'NPC_NAME_{ID}' },
},
},
Township: {
biomes: {
// Can't locate biome description localization, don't think this is exposed in game UI
name: { key: 'TOWNSHIP', idFormat: 'BIOME_{ID}' },
},
buildings: {
// Building description has no localization, as it is unused
name: { key: 'TOWNSHIP', idFormat: 'BUILDING_{ID}' },
},
jobs: {
name: { key: 'TOWNSHIP', idFormat: 'JOB_{ID}' },
},
resources: {
name: { key: 'TOWNSHIP', idFormat: 'RESOURCE_{ID}' },
},
tasks: {
// name is not exposed in game UI, and has no localization
// category is localized in transformDataNode
description: { key: 'TOWNSHIP_TASKS', idFormat: '{ID}_description' },
},
worships: {
name: { stringSpecial: 'tsWorshipName' },
statueName: { stringSpecial: 'tsWorshipStatueName' },
},
},
Woodcutting: {
trees: {
name: { key: 'TREE_NAME' },
},
},
},
};
// Determine which language key data applies
var langKeyData;
if (isSkill) {
// Combine common & skill specific keys
const skillKey = this.getLocalID(parentNode[nodeKey].skillID);
const langCommon = langKeys.skillData._common;
let langSkill = structuredClone(langKeys.skillData[skillKey]);
if (langCommon !== undefined) {
if (langSkill === undefined) {
langSkill = {};
}
Object.keys(langCommon).forEach((k) => {
if (langSkill[k] === undefined) {
langSkill[k] = {};
}
Object.keys(langCommon[k]).forEach((prop) => {
langSkill[k][prop] = langCommon[k][prop];
});
});
}
langKeyData = langSkill;
} else if (langKeys[nodeKey] !== undefined) {
langKeyData = { _root: langKeys[nodeKey] };
} else if (!hasNoLangData.includes(nodeKey)) {
console.warn('No lang key data found for ' + nodeKey);
}
if (langKeyData !== undefined) {
var dataToTranslate = parentNode[nodeKey];
if (isSkill) {
dataToTranslate = dataToTranslate.data;
}
if (!Array.isArray(dataToTranslate)) {
dataToTranslate = [dataToTranslate];
}
dataToTranslate.forEach((tData) => {
Object.keys(langKeyData).forEach((langKey) => {
const targetData = langKey === '_root' ? tData : tData[langKey];
if (targetData !== undefined) {
const targetArr = Array.isArray(targetData) ? targetData : [targetData];
targetArr.forEach((target) => {
const handlerFunc = langKeyData[langKey]['_handler'];
if (handlerFunc !== undefined) {
switch (handlerFunc) {
case 'mapPortals':
Object.keys(target).forEach((portalKey) => {
let portalData = target[portalKey];
const langID = this.getLocalID(portalData.originWorldMap) + '_' + this.getLocalID(portalData.id);
portalData.name = this.getLangString('POI_NAME', langID);
portalData.description = this.getLangString('POI_DESCRIPTION', langID);
});
break;
case 'cartoMaps':
// Target represents a world map
const mapID = this.getLocalID(target.id);
target.name = this.getLangString('WORLD_MAP_NAME', mapID);
// Process POIs
target.pointsOfInterest.forEach((poi) => {
const langID = mapID + '_' + this.getLocalID(poi.id);
poi.name = this.getLangString('POI_NAME', langID);
poi.description = this.getLangString('POI_DESCRIPTION', langID);
});
break;
}
} else {
Object.keys(langKeyData[langKey]).forEach((langPropID) => {
const langProp = langKeyData[langKey][langPropID];
if (!langProp.onlyIfExists || target[langPropID] !== undefined) {
const langIDKey = langProp.idKey ?? 'id';
var langIDValue;
if (Array.isArray(target[langIDKey])) {
// The ID key can sometimes be an array of IDs (e.g. Summoning synergies)
langIDValue = target[langIDKey].map((id) => this.getLocalID((id ?? '').toString()));
} else {
langIDValue = this.getLocalID((target[langIDKey] ?? '').toString());
}
let langIdent = langProp.idFormat;
if (langProp.idSpecial !== undefined) {
// Use a special method to determine the ID format
switch (langProp.idSpecial) {
case 'altMagicDesc':
langIdent = altMagicDescIDKey(target);
break;
case 'shopChainID':
langIdent = this.getLocalID(shopChainPropKey(target, langPropID, 'id'));
break;
}
}
if (langIdent === undefined) {
langIdent = langIDValue;
} else {
// langIdent is in a specific format
const langTemplate = {};
if (isSkill) {
langTemplate.SKILLID = this.getLocalID(parentNode[nodeKey].skillID);
}
if (Array.isArray(langIDValue)) {
langIDValue.forEach((val, idx) => {
langTemplate['ID' + idx] = this.getLocalID(val);
});
} else {
langTemplate.ID = langIDValue;
}
Object.keys(langTemplate).forEach((k) => {
langIdent = langIdent.replaceAll('{' + k + '}', langTemplate[k]);
});
}
let langCategoryKey = langProp.key;
if (langProp.keySpecial !== undefined) {
// Use a special method to determine the category key
switch (langProp.keySpecial) {
case 'shopChainKey':
langCategoryKey = shopChainPropKey(target, langPropID, 'category');
break;
}
}
if (Array.isArray(target[langPropID])) {
target[langPropID].forEach((targetElem, num) => {
const langIdentFinal = langIdent.replaceAll('{NUM}', num.toString());
const langString = this.getLangString(langCategoryKey, langIdentFinal);
target[langPropID][num] = langString;
if (this.debugMode) {
if (langString !== undefined) {
console.debug(
'Set value of property ' +
langPropID +
'[' +
num.toString() +
'] for ' +
langIdentFinal +
' in node ' +
nodeName +
' to: ' +
langString
);
} else {
console.debug(
'No translation: property ' +
langPropID +
' for ' +
langIdentFinal +
' in node ' +
nodeName
);
}
}
});
} else {
let langString;
if (langProp.stringSpecial !== undefined) {
// Use a custom function to determine the string
switch (langProp.stringSpecial) {
case 'itemDesc':
langString = itemDesc(target);
break;
case 'shopPurchaseDesc':
langString = shopPurchaseDesc(target);
break;
case 'passiveDesc':
langString = passiveDesc(target);
break;
case 'relicDesc':
langString = relicDesc(target);
break;
case 'spAttDesc':
langString = spAttDesc(target);
break;
case 'tsWorshipName':
langString = tsWorshipName(target);
break;
case 'tsWorshipStatueName':
langString = tsWorshipStatueName(target);
break;
case 'attackSpellbooksName':
langString = attackSpellbooksName(target);
break;
case 'attackSpellName':
langString = attackSpellName(target);
break;
}
} else {
langString = this.getLangString(langCategoryKey, langIdent);
}
target[langPropID] = langString;
if (this.debugMode) {
if (langString !== undefined) {
console.debug(
'Set value of property ' +
langPropID +
' for ' +
langIdent +
' in node ' +
nodeName +
' to: ' +
langString
);
} else {
console.debug(
'No translation: property ' + langPropID + ' for ' + langIdent + ' in node ' + nodeName
);
}
}
}
}
});
}
});
}
});
});
}
}
getLangString(key, identifier) {
let lookupVal = '';
if (key !== undefined) {
lookupVal = key;
}
if (identifier !== undefined) {
lookupVal += (lookupVal.length > 0 ? '_' : '') + identifier;
}
return this.customLocalizations[lookupVal] ?? loadedLangJson[lookupVal];
}
getNamespacedID(namespace, ID) {
if (ID.indexOf(':') > 0) {
return ID;
} else {
return namespace + ':' + ID;
}
}
getLocalID(ID) {
if (ID !== undefined && ID.indexOf(':') > 0) {
return ID.split(':').pop();
} else {
return ID;
}
}
}
let wd = new Wiki();
wd.printWikiData();
|