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
- 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: // Special attack description generation class Wiki { constructor() { this.getLocalID = this.getLocalID.bind(this); this.namespaces = { melvorD: { displayName: "Demo", url: "https://melvoridle.com/assets/data/melvorDemo.json" }, melvorF: { displayName: "Full Version", url: "https://melvoridle.com/assets/data/melvorFull.json" }, melvorTotH: { displayName: "Throne of the Herald", url: "https://melvoridle.com/assets/data/melvorTotH.json" } }; this.packData = {}; this.gameData = {}; 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(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(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(entityType, entity) { if (entityType !== 'skillData') { return entity; } else { const newData = structuredClone(entity); newData.forEach((i) => i.spellBook == 'altMagic'); return newData; } } }; 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) { const dataNode = parentNode[nodeKey]; const transformFunc = this.dataPropTransforms[nodeKey]; if (transformFunc !== undefined) { // A transformation function is defined for this node parentNode[nodeKey] = transformFunc(categoryName, dataNode); } else 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; } } }); } // Special case for skillData so that certain values initialized when the various Skill // classes are initialized may be added here also else if ((categoryName === 'skillData') && dataNode.skillID !== undefined && dataNode.data !== undefined) { // We are currently at the topmost level of a skill object const gameSkill = game.skills.registeredObjects.get(dataNode.skillID); if (gameSkill !== undefined) { 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': case 'melvorD:Summoning': importKeys = [ 'baseInterval' ]; break; case 'melvorD:Thieving': importKeys = [ 'baseInterval', 'baseStunInterval', 'itemChance', 'baseAreaUniqueChance' ]; break; case 'melvorD:Agility': importKeys = [ 'obstacleUnlockLevels' ]; break; case 'melvorD:Astrology': // Astrology has a number of values stored outside of gameSkill const astKeys = [ 'standardModifierLevels', 'uniqueModifierLevels', 'standardModifierCosts', 'uniqueModifierCosts', 'baseStardustChance', 'goldenStardustChance', 'baseInterval' ]; astKeys.forEach((k) => dataNode.data[k] = Astrology[k]); 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]); } if (importKeys.length > 0) { importKeys.forEach((k) => dataNode.data[k] = gameSkill[k]); } } } } 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; } // 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 '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 if (this.gameData[categoryName] === undefined) { this.gameData[categoryName] = []; } categoryData.forEach((orderData) => { switch(orderData.insertAt) { case 'Start': this.gameData[categoryName].splice(0, 0, ...orderData.ids); break; case 'End': this.gameData[categoryName].push(...orderData.ids); break; case 'Before': const beforeIdx = this.gameData[categoryName].findIndex((item) => item === orderData.beforeID); if (beforeIdx === -1) { throw new Error(`Couldn't insert before in category ${ categoryName }: Item ${ orderData.beforeID } is not in the array.`); } this.gameData[categoryName].splice(beforeIndex, 0, ...orderData.ids); break; case 'After': const afterIdx = this.gameData[categoryName].findIndex((item) => item === orderData.afterID); if (afterIdx === -1) { throw new Error(`Couldn't insert after in category ${ categoryName }: Item ${ orderData.afterID } is not in the array.`); } this.gameData[categoryName].splice(afterIdx + 1, 0, ...orderData.ids); break; } }); break; case 'golbinRaid': // Properties contain unordered arrays that need to be combined if (this.gameData[categoryName] === undefined) { this.gameData[categoryName] = categoryData; } 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] = categoryData; } else { // Find the appropriate skill object and combine properties with that categoryData.forEach((skillData) => { const skillIdx = this.gameData[categoryName].findIndex((skill) => skill.skillID === skillData.skillID); if (skillIdx === -1) { this.gameData[categoryName].push(skillData); } else { const skillObj = this.gameData[categoryName][skillIdx].data; Object.keys(skillData.data).forEach((dataKey) => { 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) => 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; } } 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(); |