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 class Wiki { constructor() { this.debugMode = false; this.prettyPrint = false; 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 = {}; }; 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; if (this.prettyPrint) { dataObjText = JSON.stringify(this.gameData, undefined, '\t'); } else { 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 'pages': case 'steamAchievements': case 'tutorialStageOrder': case 'tutorialStages': // 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 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 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 if (!this.skillDataInit[dataNode.skillID]) { const gameSkill = game.skills.getObjectByID(dataNode.skillID); if (gameSkill !== undefined) { 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.data.baseGemChance = 1; dataNode.data.rockTypes = loadedLangJson.MINING_TYPE; 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'}, {'from': 'MAX_TRADER_STOCK_INCREASE', 'to': 'maxTraderStockIncrease'}, ]; 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((i) => ({ id: i, name: gameSkill.tasks.getTownshipTaskCategoryName(i)})); 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); } } 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) { //TODO: Amend to follow { id: ..., name: ... } structure. Obtain name from getLangString('EQUIP_SLOT', numID) 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; } // 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 getLangString(entity.category, this.getLocalID(entity.id)); case 'equipmentStats': const newStats = {}; entity.forEach((stat) => { if (newStats[stat.key] === undefined) { newStats[stat.key] = stat.value; } else { newStats[stat.key] += 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 = function(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 = function(data) { // Items have varying logic based on the type of item, and the lang data contains // some incorrect stuff for items whose descriptions are generated entirely // from modifiers, so just get the description from in-game objects instead. let desc; const item = game.items.getObjectByID(data.id); if (item !== undefined) { desc = item.description; if (desc === getLangString('BANK_STRING', '38')) { // Generic "no description" string return undefined; } // Temporary fix for issue with language data keys for FrostSpark 1H Sword else if (desc.includes('UNDEFINED TRANSLATION') && data.id === 'melvorTotH:FrostSpark_1H_Sword') { return getLangString('ITEM_DESCRIPTION', 'Frostspark_1H_Sword') } else { return desc; } } } const passiveDesc = function(data) { const passive = game.combatPassives.getObjectByID(data.id); if (passive !== undefined) { return passive.description; } } const spAttDesc = function(data) { const spAtt = game.specialAttacks.getObjectByID(data.id); if (spAtt !== undefined) { return spAtt.description; } } const langKeys = { ancientSpells: { name: { key: 'MAGIC', idFormat: 'ANCIENT_NAME_{ID}' } }, archaicSpells: { name: { key: 'MAGIC', idFormat: 'ARCHAIC_NAME_{ID}' } }, 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}' } }, combatAreas: { name: { key: 'COMBAT_AREA', idFormat: 'NAME_{ID}'} }, 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}' } }, 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: true } }, lore: { title: { key: 'LORE', idFormat: 'TITLE_{ID}' } }, monsters: { name: { key: 'MONSTER_NAME' }, description: { key: 'MONSTER_DESCRIPTION' } }, pets: { name: { key: 'PET_NAME' } }, prayers: { name: { key: 'PRAYER', idFormat: 'PRAYER_NAME_{ID}' } }, shopCategories: { name: { key: 'SHOP_CAT' } }, shopPurchases: { customName: { key: 'SHOP_NAME', onlyIfExists: true }, customDescription: { key: 'SHOP_DESCRIPTION', onlyIfExists: true } }, 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}' } }, standardSpells: { name: { key: 'MAGIC', idFormat: 'SPELL_NAME_{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}' } }, categories: { name: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}' } }, customMilestones: { name: { key: 'MILESTONES', idKey: 'milestoneID' } }, masteryLevelUnlocks: { description: { key: 'MASTERY_BONUS', idKey: 'descriptionID', idFormat: '{SKILLID}_{ID}' } } }, 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}' } } }, Farming: { categories: { description: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_description' }, seedNotice: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_seedNotice' }, singularName: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_singular' } } }, Fishing: { areas: { name: { key: 'FISHING', idFormat: 'AREA_NAME_{ID}' } } }, Herblore: { recipes: { name: { key: 'POTION_NAME' } } }, Magic: { altSpells: { name: { key: 'MAGIC', idFormat: 'ALTMAGIC_NAME_{ID}' }, description: { key: 'MAGIC', idSpecial: 'altMagicDesc' } } }, Mining: { rockData: { name: { key: 'ORE_NAME' } } }, Summoning: { 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: { key: 'MONSTER_NAME' }, statueName: { key: 'TOWNSHIP', idFormat: 'Statue_of_{ID}' } } }, 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 { 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) => { 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 'passiveDesc': langString = passiveDesc(target); break; case 'spAttDesc': langString = spAttDesc(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) { const langCat = loadedLangJson[key]; if (langCat !== undefined) { return langCat[identifier]; } } 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(); |