17,101
edits
(Add MAX_TRADER_STOCK_INCREASE to townKeys for next update) |
(Update code for localization & various other bits) |
||
Line 9: | Line 9: | ||
{{SpoilerBox|color=default|title=Code|text=<pre>// TODO: | {{SpoilerBox|color=default|title=Code|text=<pre>// TODO: | ||
// Handle modifications portion of data packages | // Handle modifications portion of data packages | ||
class Wiki { | 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); | 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 | // Find the appropriate skill object and combine properties with that | ||
categoryData.forEach((skillData) => { | categoryData.forEach((skillData) => { | ||
Line 506: | Line 432: | ||
}); | }); | ||
}); | }); | ||
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) { | combineOrderedData(existingData, newData) { | ||
// Elements are inserted at a particular index, controlled by rules | // Elements are inserted at a particular index, controlled by rules | ||
Line 590: | Line 517: | ||
return resultData; | 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; | let wd = new Wiki; | ||
wd.printWikiData();</pre>}} | wd.printWikiData();</pre>}} |