Module:GameData/doc: Difference between revisions
From Melvor Idle
(Update for v1.1.2) |
(Support shop purchases for data modifications, and suppress language data warnings) |
||
Line 491: | Line 491: | ||
const modCat = modDataKeys[modCatID]; | const modCat = modDataKeys[modCatID]; | ||
const catData = modData[modCat]; | const catData = modData[modCat]; | ||
if (modCat === 'shopUpgradeChains') { | if ((modCat === 'shopUpgradeChains') || (modCat === 'shopPurchases')) { | ||
// Modify the root upgrade ID of shop upgrade chains | // Modify the root upgrade ID of shop upgrade chains, and modify attributes of shop purchases | ||
catData.forEach((modItem) => { | catData.forEach((modItem) => { | ||
const modObjID = modItem.id; | const modObjID = modItem.id; | ||
Line 843: | Line 843: | ||
} | } | ||
} | } | ||
const hasNoLangData = [ | |||
// Categories that contain no localized text. Supresses warnings about no lang data | |||
'bankSortOrder', | |||
'combatAreaDisplayOrder', | |||
'combatEvents', | |||
'dungeonDisplayOrder', | |||
'golbinRaid', | |||
'itemEffects', | |||
'itemSynergies', | |||
'itemUpgrades', | |||
'itmMonsters', | |||
'randomGems', | |||
'randomSuperiorGems', | |||
'slayerAreaDisplayOrder', | |||
'shopCategoryOrder', | |||
'shopDisplayOrder', | |||
'spiderLairMonsters', | |||
'stackingEffects' | |||
]; | |||
const langKeys = { | const langKeys = { | ||
ancientSpells: { | ancientSpells: { | ||
Line 1,052: | Line 1,071: | ||
langKeyData = { _root: langKeys[nodeKey] }; | langKeyData = { _root: langKeys[nodeKey] }; | ||
} | } | ||
else { | else if (!hasNoLangData.includes(nodeKey)) { | ||
console.warn('No lang key data found for ' + nodeKey); | console.warn('No lang key data found for ' + nodeKey); | ||
} | } |
Revision as of 23:16, 9 May 2023
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 and Module:GameData/data2 accordingly
Code |
---|
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.`);
}
});
// The output data is now long enough that it exceeds the maximum allowed MediaWiki article
// length of 2,048KB. The below determines how the data should be separated over multiple
// pages (Module:GameData then combines the data into a single structure upon
// initialization).
this.printPages = [
{ includeCategories: '*', destination: 'Module:GameData/data' },
{ includeCategories: ['items'], destination: 'Module:GameData/data2' }
];
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 + ')';
}
getObjectByID(data, objectID, idKey = 'id') {
if ((data !== undefined) && (objectID !== undefined)) {
return data.find((obj) => obj[idKey] === objectID);
}
}
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;
this.printPages.forEach((page) => {
let inclCat = [];
if (Array.isArray(page.includeCategories)) {
inclCat = page.includeCategories;
}
else if (page.includeCategories === '*') {
// Special value, include all categories other than those included within
// other pages
inclCat = Object.keys(this.gameData).filter((cat) => !this.printPages.some((p) => Array.isArray(p.includeCategories) && p.includeCategories.includes(cat)));
}
let gameDataFiltered = {};
inclCat.forEach((cat) => gameDataFiltered[cat] = wd.gameData[cat]);
const escapeQuotes = (data) => {
var newData = data.replace(/\'/g, "\\\'");
newData = newData.replace(/\\\"/g, "\\\\\"");
return newData;
};
const dataFormatter = (category, data) => {
if (category === 'skillData') {
return '"' + category + '":[' + data.map((x) => escapeQuotes(JSON.stringify(x))).join(",' ..\n'") + ']';
}
else {
return '"' + category + '":' + escapeQuotes(JSON.stringify(data));
}
};
// Convert game data into a JSON string for export
dataObjText = undefined;
if (this.prettyPrint) {
dataObjText = escapeQuotes(JSON.stringify(gameDataFiltered, undefined, '\t'));
}
else {
dataObjText = "{" + Object.keys(gameDataFiltered).map((k) => dataFormatter(k, gameDataFiltered[k])).join(",' ..\n'") + "}"; //JSON.stringify(gameDataFiltered);
}
let dataText = '-- Version: ' + this.getGameVersion();
dataText += "\r\n\r\nlocal gameData = mw.text.jsonDecode('";
dataText += dataObjText;
dataText += "')\r\n\r\nreturn gameData";
console.log(`For page "${ page.destination }" (${ dataText.length.toLocaleString() } bytes):`);
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 '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
const gameSkill = game.skills.getObjectByID(dataNode.skillID);
// 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] = this.getLangString('MASTERY_CHECKPOINT', `${ localID }_${ idx }`);
});
}
if (!this.skillDataInit[dataNode.skillID]) {
if (gameSkill !== undefined) {
// 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: 'BASE_STORAGE', to: 'baseStorage'},
{from: 'BASE_TAX_RATE', to: 'baseTaxRate'},
{from: 'DECREASED_BUILDING_COST_CAP', to: 'decreasedBuildingCostCap' },
{from: 'GP_PER_CITIZEN', to: 'gpPerCitizen'},
{from: 'MAX_WORSHIP', to: 'maxWorship'},
{from: 'MINIMUM_HEALTH', to: 'minimumHealth'},
{from: 'populationForTier', to: 'populationForTier'},
{from: 'TICK_LENGTH', to: 'tickLength'},
{from: 'RARE_SEASON_CHANCE', to: 'rareSeasonChance'},
{from: 'WORSHIP_CHANGE_COST', to: 'worshipChangeCost'},
{from: 'WORSHIP_CHECKPOINTS', to: 'worshipCheckpoints'},
];
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 'bankSortOrder':
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) => {
// Special case for Township item conversions
if ((skillObj[dataKey] !== undefined) && (skillData.skillID === 'melvorD:Township') && (dataKey === 'itemConversions')) {
Object.keys(skillData.data[dataKey]).forEach((convKey) => {
skillData.data[dataKey][convKey].forEach((resource) => {
// Find the resource if it already exists within the combined data
const resourceIdx = skillObj[dataKey][convKey].findIndex((res) => res.resourceID === resource.resourceID);
if (resourceIdx === -1) {
skillObj[dataKey][convKey].push(resource);
}
else {
skillObj[dataKey][convKey][resourceIdx].items.push(...resource.items);
}
})
});
}
else 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;
}
});
// If the data package contains modifications, apply these also
const modificationData = this.packData[namespace].modifications;
if (modificationData !== undefined) {
this.applyDataModifications(modificationData);
}
}
applyDataModifications(modData) {
const modDataKeys = Object.keys(modData)
for (const modCatID in modDataKeys) {
const modCat = modDataKeys[modCatID];
const catData = modData[modCat];
if ((modCat === 'shopUpgradeChains') || (modCat === 'shopPurchases')) {
// Modify the root upgrade ID of shop upgrade chains, and modify attributes of shop purchases
catData.forEach((modItem) => {
const modObjID = modItem.id;
if (modObjID === undefined) {
console.warn(`Could not apply data modification: ID of object to be modified not found, category "${ modCat }"`);
}
else {
const modObj = this.getObjectByID(this.gameData[modCat], modObjID);
if (modObj === undefined) {
console.warn(`Could not apply data modification: Object with ID "${ modObjID }" not found for category "${ modCat }"`);
}
else {
Object.keys(modItem).filter((k) => k !== 'id').forEach((k) => {
modObj[k] = modItem[k];
});
}
}
});
}
else if (modCat === 'cookingCategories') {
// Append to the list of shop upgrade IDs for cooking utilities/categories
catData.forEach((modItem) => {
const modObjID = modItem.id;
const cookingSkill = this.getObjectByID(this.gameData.skillData, 'melvorD:Cooking', 'skillID');
if (modObjID === undefined) {
console.warn(`Could not apply data modification: ID of object to be modified not found, category "${ modCat }"`);
}
else if (cookingSkill === undefined) {
console.warn('Could not apply data modification: Data for skill "melvorD:Cooking" not found');
}
else {
const modObj = this.getObjectByID(cookingSkill.data.categories, modObjID);
if (modObj === undefined) {
console.warn(`Could not apply data modification: Object with ID "${ modObjID }" not found for category "${ modCat }"`);
}
else {
Object.keys(modItem).filter((k) => k !== 'id').forEach((k) => {
if (k === 'shopUpgradeIDs') {
if (modObj[k] === undefined) {
modObj[k] = modItem[k];
}
else {
modObj[k].push(...modItem[k]);
}
}
else {
console.warn(`Could not apply data modification: Unhandled key "${ k }" for category "${ modCat }", object "${ mobObjID }"`);
}
});
}
}
});
}
else if (modCat === 'fletchingRecipes') {
// Append to alternativeCosts property of recipes (e.g. Arrow shafts)
catData.forEach((modItem) => {
const modObjID = modItem.id;
const fletchingSkill = this.getObjectByID(this.gameData.skillData, 'melvorD:Fletching', 'skillID');
if (modObjID === undefined) {
console.warn(`Could not apply data modification: ID of object to be modified not found, category "${ modCat }"`);
}
else if (fletchingSkill === undefined) {
console.warn('Could not apply data modification: Data for skill "melvorD:Fletching" not found');
}
else {
const modObj = this.getObjectByID(fletchingSkill.data.recipes, modObjID);
if (modObj === undefined) {
console.warn(`Could not apply data modification: Object with ID "${ modObjID }" not found for category "${ modCat }"`);
}
else {
Object.keys(modItem).filter((k) => k !== 'id').forEach((k) => {
if (k === 'alternativeCosts') {
if (modObj[k] === undefined) {
modObj[k] = modItem[k];
}
else {
modObj[k].push(...modItem[k]);
}
}
else {
console.warn(`Could not apply data modification: Unhandled key "${ k }" for category "${ modCat }", object "${ mobObjID }"`);
}
});
}
}
});
}
else {
console.warn(`Could not apply data modification: Unhandled category "${ modCat }"`);
}
}
}
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) {
const slotIDs = Object.keys(EquipmentSlots).filter((slotID) => !isNaN(parseInt(slotID)));
this.gameData.equipmentSlots = slotIDs.map((slotID) => ({id: EquipmentSlots[slotID], name: this.getLangString('EQUIP_SLOT', slotID)}));
}
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;
}
if (this.gameData.modifierData === undefined && modifierData !== undefined) {
var wikiModData = {};
Object.keys(modifierData).forEach((modK) => {
const mod = modifierData[modK];
wikiModData[modK] = {};
Object.keys(mod).forEach((k) => {
if (k === 'modifyValue') {
// Convert function into function name
// If the function is inline and has no name, then use the function definition instead
var funcName = mod[k].name;
if (funcName === 'modifyValue') {
funcName = mod[k].toString();
}
wikiModData[modK][k] = funcName;
}
else if (k === 'langDescription') {
wikiModData[modK]['description'] = mod[k];
}
else if (k !== 'description') {
wikiModData[modK][k] = mod[k];
}
});
});
this.gameData.modifierData = wikiModData;
}
}
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 this.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 = (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 = (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 === this.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 this.getLangString('ITEM_DESCRIPTION', 'Frostspark_1H_Sword')
}
else {
return desc;
}
}
}
const passiveDesc = (data) => {
const passive = game.combatPassives.getObjectByID(data.id);
if (passive !== undefined) {
return passive.description;
}
}
const spAttDesc = (data) => {
const spAtt = game.specialAttacks.getObjectByID(data.id);
if (spAtt !== undefined) {
return spAtt.description;
}
}
const tsWorshipName = (data) => {
const worship = game.township.worships.getObjectByID(data.id);
if (worship !== undefined) {
return worship.name;
}
}
const tsWorshipStatueName = (data) => {
const worship = game.township.worships.getObjectByID(data.id);
if (worship !== undefined) {
return worship.statueName;
}
}
const hasNoLangData = [
// Categories that contain no localized text. Supresses warnings about no lang data
'bankSortOrder',
'combatAreaDisplayOrder',
'combatEvents',
'dungeonDisplayOrder',
'golbinRaid',
'itemEffects',
'itemSynergies',
'itemUpgrades',
'itmMonsters',
'randomGems',
'randomSuperiorGems',
'slayerAreaDisplayOrder',
'shopCategoryOrder',
'shopDisplayOrder',
'spiderLairMonsters',
'stackingEffects'
];
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: { stringSpecial: 'tsWorshipName' },
statueName: { stringSpecial: 'tsWorshipStatueName' }
}
},
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 if (!hasNoLangData.includes(nodeKey)) {
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;
case 'tsWorshipName':
langString = tsWorshipName(target);
break;
case 'tsWorshipStatueName':
langString = tsWorshipStatueName(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) {
return loadedLangJson[key + '_' + 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();
|