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: // Additional properties for some skills (e.g. Firemaking) defined within the JS its self at runtime - add from game // Special attack description generation class Wiki { constructor() { this.getLocalID = this.getLocalID.bind(this); this.namespaces = { melvorD: { displayName: "Demo", url: "https://test.melvoridle.com/dlcPrep/assets/data/melvorDemo.json" }, melvorF: { displayName: "Full Version", url: "https://test.melvoridle.com/dlcPrep/assets/data/melvorFull.json" }, melvorTotH: { displayName: "Throne of the Herald", url: "https://test.melvoridle.com/dlcPrep/assets/data/melvorTotH.json" } }; this.packData = {}; this.gameData = {}; this.gameData.namespaces = {}; 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; }, occupiesSlots: function(entityType, entity) { return entity.occupiesSlots.length > 0; } }; 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; } }; this.dataPropTransforms.langCustomDescription = this.dataPropTransforms.langHint; this.skillDataKeyRemaps = [ { id: "melvorD:Woodcutting", maps: [{ from: "trees", to: "actions" }] }, { id: "melvorD:Fishing", maps: [{ from: "fish", to: "actions" }] }, { id: "melvorD:Firemaking", maps: [{ from: "logs", to: "actions" }] }, { id: "melvorD:Cooking", maps: [{ from: "recipes", to: "actions" }] }, { id: "melvorD:Mining", maps: [{ from: "rockData", to: "actions" }] }, ]; }; 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.data; 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) => { 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 { // Check if property is to be deleted or not const filterFunc = this.dataPropFilters[key]; if (filterFunc !== undefined && !filterFunc(categoryName, dataNode)) { delete 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; } } } }); } } _Old_transformDataNode(ns, categoryName, dataNode) { if (Array.isArray(dataNode)) { // Recursive call to ensure all data is transformed, regardless of its depth dataNode.forEach((entity) => this.transformDataNode(ns, categoryName, entity)); // Are the elements of the array objects & do these objects have IDs? // If so convert the array to an object with keys equal to those IDs if (typeof dataNode[0] === 'object' && !Array.isArray(dataNode[0]) && Object.keys(dataNode[0]).length > 2) { let idKeyName = undefined; if (dataNode[0].id !== undefined) { idKeyName = "id"; } else if (dataNode[0].skillID !== undefined) { idKeyName = "skillID"; } if (idKeyName !== undefined) { const newObj = {}; dataNode.forEach((entity) => { const idKey = entity[idKeyName]; delete entity[idKeyName]; newObj[idKey] = entity; }); if (categoryName === "items") { console.log(newObj); } dataNode = newObj; } } } else if (typeof dataNode === 'object' && dataNode !== null) { // Iterate properties of object, checking if each should be deleted or transformed Object.keys(dataNode).forEach((key) => { 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 { // Check if property is to be deleted or not const filterFunc = this.dataPropFilters[key]; if (filterFunc !== undefined && !filterFunc(categoryName, dataNode)) { delete 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; } } } }); } } registerDataPackage(namespace) { // Consolidates the data package identified by namespace with existing data within // this.gameData const packData = this.packData[namespace]; if (packData === undefined) { throw new Error(`Couldn't find data for package ${ namespace }`); } // Add namespace entry to game data this.gameData.namespaces[namespace] = this.namespaces[namespace]; // Consolidate data Object.keys(packData).forEach((categoryName) => { const categoryData = packData[categoryName]; // 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 '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 '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; } }); } 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(); |