Module:GameData/doc
This is the documentation page for Module:GameData
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 accordingly
Code |
---|
// TODO: // Handle modifications portion of data packages // Use actual descriptions as per language data class Wiki { constructor() { this.namespaces = { melvorD: { displayName: "Demo", url: "https://" + location.hostname + "/assets/data/melvorDemo.json" }, melvorF: { displayName: "Full Version", url: "https://" + location.hostname + "/assets/data/melvorFull.json" }, melvorTotH: { displayName: "Throne of the Herald", url: "https://" + location.hostname + "/assets/data/melvorTotH.json" } }; // 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.`); } }); this.packData = {}; this.gameData = {}; this.skillDataInit = {}; this.dataPropFilters = { // Specifies rules for properties of entities (items, monsters, etc.) which // will be removed as they are unused in the wiki & would otherwise bloat // the data. // Format: property: ruleFunction(entityType, entity) // where ruleFunction is a function returning true if the property is to // be retained, false otherwise media: function(entityType, entity) { return false; }, altMedia: function(entityType, entity) { return false; }, markMedia: function(entityType, entity) { return false; }, icon: function(entityType, entity) { return false; }, barStyle: function(entityType, entity) { return false; }, // See: melvorD:Compost buttonStyle: function(entityType, entity) { return false; }, descriptionGenerator: function(entityType, entity) { return false; }, containerID: function(entityType, entity) { return false; }, headerBgClass: function(entityType, entity) { return false; }, textClass: function(entityType, entity) { return false; }, btnClass: function(entityType, entity) { return false; }, golbinRaidExclusive: function(entityType, entity) { return entity.golbinRaidExclusive; }, ignoreCompletion: function(entityType, entity) { return entity.ignoreCompletion; }, obtainFromItemLog: function(entityType, entity) { return entity.obtainFromItemLog; }, validSlots: function(entityType, entity) { return entity.validSlots.length > 0; }, occupiesSlots: function(entityType, entity) { return entity.occupiesSlots.length > 0; }, equipRequirements: function(entityType, entity) { return entity.equipRequirements.length > 0; }, equipmentStats: function(entityType, entity) { return entity.equipmentStats.length > 0; }, tier: function(entityType, entity) { if (entityType === 'items') { return entity.tier !== 'none'; } else { return true; } } }; this.dataPropTransforms = { // Specifies rules for transforming values of entity properties // Format: property: ruleFunction(entityType, entity) // where ruleFunction is a function returning the transformed value // to be used in place of the original value langHint: function(ns, entityType, entity) { let localID = '' if (entity.id.indexOf(':') > 0) { localID = entity.id.split(':').pop(); } else { localID = entity.id } return getLangString(entity.category, localID); }, equipmentStats: function(ns, entityType, entity) { const newStats = {}; entity.forEach((stat) => { if (newStats[stat.key] === undefined) { newStats[stat.key] = stat.value; } else { newStats[stat.key] += stat.value; } }); return newStats; }, altSpells: function(ns, entityType, entity) { if (entityType !== 'skillData') { return entity; } else { const newData = structuredClone(entity); newData.forEach((i) => { i.spellBook = 'altMagic'; }); return newData; } }, attacks: function(ns, entityType, entity) { if (entityType !== 'attacks') { return entity; } else { entity.forEach((attDef) => { const nsAttID = ns + ':' + attDef.id; const att = game.specialAttacks.getObjectByID(nsAttID); attDef.description = att.description; }); return entity; } } }; this.dataPropTransforms.langCustomDescription = this.dataPropTransforms.langHint; }; 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.namespaces[ns].url; 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`); } // All data packages should now be within this.gameData } getGameVersion() { const fileDOM = document.querySelector('#sidebar ul.nav-main'); let fileVer = "Unknown"; if (fileDOM !== null && fileDOM.dataset !== undefined) { fileVer = fileDOM.dataset.fileVersion; } return gameVersion + ' (' + fileVer + ')'; } async printWikiData() { if (!isLoaded) { throw new Error('Game must be loaded into a character first'); } if (Object.keys(this.packData).length < Object.keys(this.namespaces).length) { // Need to retrieve game data first const result = await this.getWikiData(); } let dataObjText = JSON.stringify(this.gameData); dataObjText = dataObjText.replace(/\'/g, "\\\'"); dataObjText = dataObjText.replace(/\\\"/g, "\\\\\""); let dataText = '-- Version: ' + this.getGameVersion(); dataText += "\r\n\r\nlocal gameData = mw.text.jsonDecode('"; dataText += dataObjText; dataText += "')\r\n\r\nreturn gameData"; 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).forEach((categoryName) => { switch(categoryName) { case 'bankSortOrder': case 'steamAchievements': // This data serves no purpose for the wiki and only serves to bloat // the data, so simply delete it delete packData[categoryName]; break; default: this.transformDataNode(ns, categoryName, packData, categoryName); break; } }); } transformDataNode(ns, categoryName, parentNode, nodeKey) { let dataNode = parentNode[nodeKey]; const transformFunc = this.dataPropTransforms[nodeKey]; if (transformFunc !== undefined) { // A transformation function is defined for this node parentNode[nodeKey] = transformFunc(ns, categoryName, dataNode); 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 const filterFunc = this.dataPropFilters[key]; if (filterFunc !== undefined && !filterFunc(categoryName, dataNode)) { 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 dataNode[key] = this.getNamespacedID(ns, dataNode[key]); break; case 'name': // Some items have underscores in their names, replace with spaces if (categoryName == 'items') { dataNode[key] = dataNode[key].replaceAll('_', ' '); } break; } } }); } // 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 && !this.skillDataInit[dataNode.skillID]) { // We are currently at the topmost level of a skill object const gameSkill = game.skills.registeredObjects.get(dataNode.skillID); if (gameSkill !== undefined) { dataNode.data.name = getLangString('SKILL_NAME', this.getLocalID(dataNode.skillID)); if (gameSkill.milestones !== undefined && dataNode.data.milestoneCount === undefined) { dataNode.data.milestoneCount = gameSkill.milestones.length; } // 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] = getLangString('MASTERY_CHECKPOINT', `${ localID }_${ idx }`); }); } // Import other attributes varying by skill let importKeys = []; switch(dataNode.skillID) { case 'melvorD:Firemaking': importKeys = [ 'baseAshChance', 'baseStardustChance', 'baseCharcoalChance' ]; break; case 'melvorD:Mining': importKeys = [ 'baseInterval', 'baseRockHP', 'passiveRegenInterval' ]; dataNode.baseGemChance = 1; 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:Agility': importKeys = [ 'obstacleUnlockLevels' ]; break; case 'melvorD:Summoning': importKeys = [ 'baseInterval' ]; const sumKeys = [ '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', 'standardModifierCosts', 'uniqueModifierCosts', 'baseStardustChance', 'baseGoldenStardustChance', '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': 'TICK_LENGTH', 'to': 'tickLength'}, {'from': 'MAX_TOWN_SIZE', 'to': 'maxTownSize'}, {'from': 'SECTION_SIZE', 'to': 'sectionSize'}, {'from': 'INITIAL_CITIZEN_COUNT', 'to': 'initialCitizenCount'}, {'from': 'MIN_WORKER_AGE', 'to': 'minWorkerAge'}, {'from': 'MAX_WORKER_AGE', 'to': 'maxWorkerAge'}, {'from': 'AGE_OF_DEATH', 'to': 'ageOfDeath'}, {'from': 'MIN_MIGRATION_AGE', 'to': 'minMigrationAge'}, {'from': 'MAX_MIGRATION_AGE', 'to': 'maxMigrationAge'}, {'from': 'BASE_TAX_RATE', 'to': 'baseTaxRate'}, {'from': 'EDUCATION_PER_CITIZEN', 'to': 'educationPerCitizen'}, {'from': 'HAPPINESS_PER_CITIZEN', 'to': 'happinessPerCitizen'}, {'from': 'CITIZEN_FOOD_USAGE', 'to': 'citizenFoodUsage'}, {'from': 'POPULATION_REQUIRED_FOR_BIRTH', 'to': 'populationRequiredForBirth'}, {'from': 'BASE_STORAGE', 'to': 'baseStorage'}, {'from': 'WORSHIP_CHECKPOINTS', 'to': 'worshipCheckpoints'}, {'from': 'MAX_WORSHIP', 'to': 'maxWorship'}, {'from': 'populationForTier', 'to': 'populationForTier'}, ]; townKeys.forEach((k) => dataNode.data[k.to] = gameSkill[k.from]); break; } if (importKeys.length > 0) { importKeys.forEach((k) => dataNode.data[k] = gameSkill[k]); } } this.skillDataInit[dataNode.skillID] = true; } } 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 Object.keys(packData).forEach((categoryName) => { let categoryData = packData[categoryName]; // Some data is adjusted before combining - do this here if (['combatAreas', 'dungeons', 'slayerAreas'].includes(categoryName)) { // Add area type to each area object const areaTypes = { 'combatAreas': 'combatArea', 'dungeons': 'dungeon', 'slayerAreas': 'slayerArea' } const areaType = areaTypes[categoryName]; const newData = structuredClone(categoryData); newData.forEach((x) => x.type = areaType); categoryData = newData; } else if (['ancientSpells', 'archaicSpells', 'auroraSpells', 'curseSpells', 'standardSpells'].includes(categoryName)) { // For spell books, add the spell type to each spell object. // Alt Magic spells are handled elsewhere, as they are within a skill object const spellType = categoryName.replace('Spells', ''); const newData = structuredClone(categoryData); newData.forEach((x) => x.spellBook = spellType); 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 'ancientSpells': case 'archaicSpells': case 'attackStyles': case 'attacks': case 'auroraSpells': case 'combatAreas': case 'combatEvents': case 'combatPassives': case 'curseSpells': case 'dungeons': case 'gamemodes': case 'itemEffects': case 'itemSynergies': case 'itemUpgrades': case 'itmMonsters': case 'items': case 'lore': case 'monsters': case 'pages': case 'pets': case 'prayers': case 'randomGems': case 'randomSuperiorGems': case 'shopCategories': case 'shopPurchases': case 'shopUpgradeChains': case 'slayerAreas': case 'stackingEffects': case 'standardSpells': 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 'combatAreaDisplayOrder': case 'dungeonDisplayOrder': case 'shopCategoryOrder': case 'shopDisplayOrder': 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) => { 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; } }); } registerNonPackData() { // Some data resides outside of packages. Add any such data to this.gameData within this function 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.combatTriangles === undefined) { const ctData = []; Object.keys(COMBAT_TRIANGLE_IDS).forEach((id) => { const newObj = structuredClone(combatTriangle[COMBAT_TRIANGLE_IDS[id]]); newObj.id = id; ctData.push(newObj); }); this.gameData.combatTriangles = ctData; } if (this.gameData.masteryCheckpoints === undefined) { this.gameData.masteryCheckpoints = masteryCheckpoints; } if (this.gameData.combatAreaDifficulties === undefined) { this.gameData.combatAreaDifficulties = CombatAreaMenu.difficulty.map((i) => i.name); } if (this.gameData.equipmentSlots === undefined) { this.gameData.equipmentSlots = EquipmentSlots; } if (this.gameData.attackTypes === undefined) { this.gameData.attackTypes = AttackTypeID; } if (this.gameData.slayerTiers === undefined) { const newData = structuredClone(SlayerTask.data) newData.forEach((tier) => delete tier.engDisplay); this.gameData.slayerTiers = newData; } } 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(beforeIndex, 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; } getNamespacedID(namespace, ID) { if (ID.indexOf(':') > 0) { return ID; } else { return namespace + ':' + ID; } } getLocalID(ID) { if (ID.indexOf(':') > 0) { return ID.split(':').pop(); } else { return ID; } } } let wd = new Wiki; wd.printWikiData(); |