Module:GameData/doc: Difference between revisions

Update to support 2023 Birthday event
(Add MAX_TRADER_STOCK_INCREASE to townKeys for next update)
(Update to support 2023 Birthday event)
 
(13 intermediate revisions by the same user not shown)
Line 1: Line 1:
The '''GameData''' module is the source of all game data which many other Lua modules rely upon. This module deals with the initial loading of the game data structure, and then enables other modules to access this both via a library of functions (preferred) and in its raw format.
To generate game data, do the following:
To generate game data, do the following:
# Navigate to https://melvoridle.com within your preferred web browser
# Navigate to https://melvoridle.com within your preferred web browser
Line 5: Line 7:
# Open the browser console/developer mode (usually by hitting the F12 key for most browsers)
# 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
# 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
# Copy the game data & update [[Module:GameData/data]] and [[Module:GameData/data2]] accordingly


{{SpoilerBox|color=default|title=Code|text=<pre>// TODO:
{{SpoilerBox|color=default|title=Code|text=<syntaxhighlight lang="javascript" line>class Wiki {
// Handle modifications portion of data packages
constructor() {
// Use actual descriptions as per language data
this.debugMode = false;
class Wiki {
this.prettyPrint = false;
    constructor() {
this.baseDir = "/assets/data/";
        this.namespaces = {
this.namespaces = {
            melvorD: { displayName: "Demo", url: "https://" + location.hostname + "/assets/data/melvorDemo.json" },
melvorD: { displayName: "Demo", url: "https://" + location.hostname + this.baseDir + "melvorDemo.json" },
            melvorF: { displayName: "Full Version", url: "https://" + location.hostname + "/assets/data/melvorFull.json" },
melvorF: { displayName: "Full Version", url: "https://" + location.hostname + this.baseDir + "melvorFull.json" },
            melvorTotH: { displayName: "Throne of the Herald", url: "https://" + location.hostname + "/assets/data/melvorTotH.json" }
melvorTotH: { displayName: "Throne of the Herald", url: "https://" + location.hostname + this.baseDir + "melvorTotH.json" },
        };
melvorAoD: { displayName: "Atlas of Discovery", url: "https://" + location.hostname + this.baseDir + "melvorExpansion2.json" },
        // Check all required namespaces are registered, as there are still some bits of data extracted from in-game rather than the data packages
melvorBirthday2023: { displayName: "Melvor Birthday 2023", url: "https://" + location.hostname + this.baseDir + "melvorBirthday2023.json" }
        Object.keys(this.namespaces).forEach((nsID) => {
};
            const nsTest = game.registeredNamespaces.getNamespace(nsID);
// Check all required namespaces are registered, as there are still some bits of data extracted from in-game rather than the data packages
            if (nsTest === undefined) {
Object.keys(this.namespaces).forEach((nsID) => {
                throw new Error(`Namespace ${ nsID } (${ this.namespaces[nsID].displayName }) is not registered - Ensure you are signed in and have the expansion.`);
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.maxPageBytes = 2*1024**2; // 2048KB
this.printPages = [
{ includeCategories: '*', destination: 'Module:GameData/data' },
{ includeCategories: ['items', 'itemUpgrades', 'itemSynergies', 'modifierData', 'shopPurchases'], destination: 'Module:GameData/data2' }
];


        this.packData = {};
this.packData = {};
        this.gameData = {};
this.gameData = {};
        this.skillDataInit = {};
this.skillDataInit = {};
        this.dataPropFilters = {
};
            // Specifies rules for properties of entities (items, monsters, etc.) which
async getWikiData() {
            // will be removed as they are unused in the wiki & would otherwise bloat
if (!isLoaded) {
            // the data.
throw new Error('Game must be loaded into a character first');
            // Format: property: ruleFunction(entityType, entity)
}
            //      where ruleFunction is a function returning true if the property is to
for (const nsIdx in Object.keys(this.namespaces)) {
            //      be retained, false otherwise
const ns = Object.keys(this.namespaces)[nsIdx];
            media: function(entityType, entity) { return false; },
const dataURL = this.namespaces[ns].url;
            altMedia: function(entityType, entity) { return false; },
console.log(`URL: ${ dataURL }`);
            markMedia: function(entityType, entity) { return false; },
const dataPackage = await this.getDataPackage(dataURL);
            icon: function(entityType, entity) { return false; },
if (dataPackage.namespace === undefined) {
            barStyle: function(entityType, entity) { return false; }, // See: melvorD:Compost
throw new Error(`Data package has no namespace: ${ dataURL }`);
            buttonStyle: function(entityType, entity) { return false; },
}
            descriptionGenerator: function(entityType, entity) { return false; },
else if (dataPackage.data === undefined) {
            containerID: function(entityType, entity) { return false; },
throw new Error(`Data package has no data: ${ dataURL }`);
            headerBgClass: function(entityType, entity) { return false; },
}
            textClass: function(entityType, entity) { return false; },
console.log(`Obtained data for namespace ${ dataPackage.namespace }, ${ JSON.stringify(dataPackage.data).length.toLocaleString() } bytes`);
            btnClass: function(entityType, entity) { return false; },
this.processDataPackage(dataPackage);
            golbinRaidExclusive: function(entityType, entity) { return entity.golbinRaidExclusive; },
console.log(`After transformation: ${ JSON.stringify(dataPackage.data).length.toLocaleString() } bytes`);
            ignoreCompletion: function(entityType, entity) { return entity.ignoreCompletion; },
}
            obtainFromItemLog: function(entityType, entity) { return entity.obtainFromItemLog; },
// All data packages should now be within this.gameData
            validSlots: function(entityType, entity) { return entity.validSlots.length > 0; },
}
            occupiesSlots: function(entityType, entity) { return entity.occupiesSlots.length > 0; },
getGameVersion() {
            equipRequirements: function(entityType, entity) { return entity.equipRequirements.length > 0; },
const fileDOM = document.querySelector('#sidebar ul.nav-main');
            equipmentStats: function(entityType, entity) { return entity.equipmentStats.length > 0; },
let fileVer = "Unknown";
            tier: function(entityType, entity) {
if (fileDOM !== null && fileDOM.dataset !== undefined) {
                if (entityType === 'items') {
fileVer = fileDOM.dataset.fileVersion;
                    return entity.tier !== 'none';
}
                }
return gameVersion + ' (' + fileVer + ')';
                else {
}
                    return true;
getObjectByID(data, objectID, idKey = 'id') {
                }
if ((data !== undefined) && (objectID !== undefined)) {
            }
return data.find((obj) => obj[idKey] === objectID);
        };
}
        this.dataPropTransforms = {
}
            // Specifies rules for transforming values of entity properties
getCategoriesForPage(page) {
            // Format: property: ruleFunction(entityType, entity)
if (Array.isArray(page.includeCategories)) {
            //      where ruleFunction is a function returning the transformed value
return page.includeCategories;
            //      to be used in place of the original value
}
            langHint: function(ns, entityType, entity) {
else if (page.includeCategories === '*') {
                let localID = ''
// Special value, include all categories other than those included within
                if (entity.id.indexOf(':') > 0) {
// other pages
                    localID = entity.id.split(':').pop();
return Object.keys(this.gameData).filter((cat) => !this.printPages.some((p) => Array.isArray(p.includeCategories) && p.includeCategories.includes(cat)));
                }
}
                else {
}
                    localID = entity.id
escapeQuotes(data) {
                }
var newData = data.replace(/\\/g, '\\\\')
                return getLangString(entity.category, localID);
newData = newData.replace(/'/g, "\\'");
            },
newData = newData.replace(/"/g, '\\"');
            equipmentStats: function(ns, entityType, entity) {
return newData;
                const newStats = {};
}
                entity.forEach((stat) => {
formatJSONData(category, data) {
                    if (newStats[stat.key] === undefined) {
if (data === undefined) {
                        newStats[stat.key] = stat.value;
console.warn(`dataFormatter: Data for category ${ category } is undefined`);
                    }
return '';
                    else {
}
                        newStats[stat.key] += stat.value;
if (this.debugMode) {
                    }
console.debug('Formatting category data: ' + category);
                });
}
                return newStats;
if (category === 'skillData') {
            },
return '"' + category + '":[' + data.map((x) => this.escapeQuotes(JSON.stringify(x))).join(",' ..\n'") + ']';
            altSpells: function(ns, entityType, entity) {
}
                if (entityType !== 'skillData') {
else {
                    return entity;
return '"' + category + '":' + this.escapeQuotes(JSON.stringify(data));
                }
}
                else {
}
                    const newData = structuredClone(entity);
dataFullyLoaded() {
                    newData.forEach((i) => {
return Object.keys(this.packData).length >= Object.keys(this.namespaces).length;
                        i.spellBook = 'altMagic';
}
                    });
printCategoryDataLength() {
                    return newData;
if (!this.dataFullyLoaded()) {
                }
throw new Error('Game data not loaded, use printWikiData first');
            },
}
            attacks: function(ns, entityType, entity) {
let dataLengths = [];
                if (entityType !== 'attacks') {
this.printPages.forEach((page) => {
                    return entity;
const inclCat = this.getCategoriesForPage(page);
                }
inclCat.forEach((cat) => {
                else {
dataLengths.push(({
                    entity.forEach((attDef) => {
page: page.destination,
                        const nsAttID = ns + ':' + attDef.id;
category: cat,
                        const att = game.specialAttacks.getObjectByID(nsAttID);
length: this.formatJSONData(cat, this.gameData[cat]).length
                        attDef.description = att.description;
}));
                    });
});
                    return entity;
});
                }
console.table(dataLengths);
            }
}
        };
async printWikiData() {
        this.dataPropTransforms.langCustomDescription = this.dataPropTransforms.langHint;
if (!isLoaded) {
    };
throw new Error('Game must be loaded into a character first');
    async getWikiData() {
}
        if (!isLoaded) {
if (!this.dataFullyLoaded()) {
            throw new Error('Game must be loaded into a character first');
// Need to retrieve game data first
        }
const result = await this.getWikiData();
        for (const nsIdx in Object.keys(this.namespaces)) {
}
            const ns = Object.keys(this.namespaces)[nsIdx];
console.log('Printing data for game version ' + this.getGameVersion());
            const dataURL = this.namespaces[ns].url;
this.printPages.forEach((page) => {
            console.log(`URL: ${ dataURL }`);
const inclCat = this.getCategoriesForPage(page);
            const dataPackage = await this.getDataPackage(dataURL);
let gameDataFiltered = {};
            if (dataPackage.namespace === undefined) {
inclCat.forEach((cat) => gameDataFiltered[cat] = this.gameData[cat]);
                throw new Error(`Data package has no namespace: ${ dataURL }`);
            }
            else if (dataPackage.data === undefined) {
                throw new Error(`Data package has no data: ${ dataURL }`);
            }
            console.log(`Obtained data for namespace ${ dataPackage.namespace }, ${ JSON.stringify(dataPackage.data).length.toLocaleString() } bytes`);
            this.processDataPackage(dataPackage);
            console.log(`After transformation: ${ JSON.stringify(dataPackage.data).length.toLocaleString() } bytes`);
        }
        // All data packages should now be within this.gameData
    }
    getGameVersion() {
        const fileDOM = document.querySelector('#sidebar ul.nav-main');
        let fileVer = "Unknown";
        if (fileDOM !== null && fileDOM.dataset !== undefined) {
            fileVer = fileDOM.dataset.fileVersion;
        }
        return gameVersion + ' (' + fileVer + ')';
    }
    async printWikiData() {
        if (!isLoaded) {
            throw new Error('Game must be loaded into a character first');
        }
        if (Object.keys(this.packData).length < Object.keys(this.namespaces).length) {
            // Need to retrieve game data first
            const result = await this.getWikiData();
        }
        let dataObjText = JSON.stringify(this.gameData);
        dataObjText = dataObjText.replace(/\'/g, "\\\'");
        dataObjText = dataObjText.replace(/\\\"/g, "\\\\\"");


        let dataText = '-- Version: ' + this.getGameVersion();
// Convert game data into a JSON string for export
        dataText += "\r\n\r\nlocal gameData = mw.text.jsonDecode('";
let dataText;
        dataText += dataObjText;
if (this.prettyPrint) {
        dataText += "')\r\n\r\nreturn gameData";
dataText = JSON.stringify(gameDataFiltered, undefined, '\t');
        console.log(dataText);
}
    }
else {
    async getDataPackage(url) {
dataText = JSON.stringify(gameDataFiltered);
        // 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);
console.log(`For page "${ page.destination }" (${ dataText.length.toLocaleString() } bytes):`);
        this.packData[dataPackage.namespace] = dataPackage;
if (dataText.length > this.maxPageBytes) {
        this.registerDataPackage(dataPackage.namespace);
console.warn(`Page "${ page.destination }" exceeds max page size of ${ (this.maxPageBytes / 1024).toLocaleString() }KB by ${ (dataText.length - this.maxPageBytes).toLocaleString() } bytes. Consider amending the printPages configuration to move some data categories from this page onto other pages.`)
    }
}
    transformDataPackage(dataPackage) {
console.log(dataText);
        // Takes a raw data package and performs various manipulations
});
        const ns = dataPackage.namespace;
}
        const packData = dataPackage.data;
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;


        Object.keys(packData).forEach((categoryName) => {
this.transformDataPackage(dataPackage);
            switch(categoryName) {
this.packData[dataPackage.namespace] = dataPackage;
                case 'bankSortOrder':
this.registerDataPackage(dataPackage.namespace);
                case 'steamAchievements':
}
                    // This data serves no purpose for the wiki and only serves to bloat
transformDataPackage(dataPackage) {
                    // the data, so simply delete it
// Takes a raw data package and performs various manipulations
                    delete packData[categoryName];
const ns = dataPackage.namespace;
                    break;
const packData = dataPackage.data;
                default:
                    this.transformDataNode(ns, categoryName, packData, categoryName);
                    break;
            }
        });
    }
    transformDataNode(ns, categoryName, parentNode, nodeKey) {
        let dataNode = parentNode[nodeKey];
        const transformFunc = this.dataPropTransforms[nodeKey];
        if (transformFunc !== undefined) {
            // A transformation function is defined for this node
            parentNode[nodeKey] = transformFunc(ns, categoryName, dataNode);
            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
                const filterFunc = this.dataPropFilters[key];
                if (filterFunc !== undefined && !filterFunc(categoryName, dataNode)) {
                    delete dataNode[key];
                }
                else if (typeof dataNode[key] === "object" && dataNode[key] !== null) {
                    // If an object (either an array or key/value store) is within the current
                    // object then we must traverse this too
                    this.transformDataNode(ns, categoryName, dataNode, key);
                }
                else {
                    // Transform property, if a transformation is defined below
                    switch(key) {
                        case 'id':
                            // Add namespace to ID if it isn't already
                            dataNode[key] = this.getNamespacedID(ns, dataNode[key]);
                            break;
                        case 'name':
                            // Some items have underscores in their names, replace with spaces
                            if (categoryName == 'items') {
                                dataNode[key] = dataNode[key].replaceAll('_', ' ');
                            }
                            break;
                    }
                }
            });
        }
        // 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 && !this.skillDataInit[dataNode.skillID]) {
            // We are currently at the topmost level of a skill object
            const gameSkill = game.skills.registeredObjects.get(dataNode.skillID);
            if (gameSkill !== undefined) {
                dataNode.data.name = getLangString('SKILL_NAME', this.getLocalID(dataNode.skillID));


                if (gameSkill.milestones !== undefined && dataNode.data.milestoneCount === undefined) {
Object.keys(packData).forEach((categoryName) => {
                    dataNode.data.milestoneCount = gameSkill.milestones.length;
switch(categoryName) {
                }
case 'pages':
                // For every skill with mastery, add mastery checkpoint descriptions
case 'steamAchievements':
                if (gameSkill instanceof SkillWithMastery && dataNode.data.masteryTokenID !== undefined && dataNode.data.masteryCheckpoints === undefined) {
case 'tutorialStageOrder':
                    const localID = this.getLocalID(dataNode.skillID);
case 'tutorialStages':
                    dataNode.data.baseMasteryPoolCap = gameSkill.baseMasteryPoolCap;
// This data serves no purpose for the wiki and only serves to bloat
                    dataNode.data.masteryCheckpoints = [];
// the data, so simply delete it
                    masteryCheckpoints.forEach((pct, idx) => {
delete packData[categoryName];
                        dataNode.data.masteryCheckpoints[idx] = getLangString('MASTERY_CHECKPOINT', `${ localID }_${ idx }`);
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);
}


                // Import other attributes varying by skill
// Special case for skillData so that certain values initialized when the various Skill
                let importKeys = [];
// classes are initialized may be added here also
                switch(dataNode.skillID) {
if ((categoryName === 'skillData') && dataNode.skillID !== undefined && dataNode.data !== undefined) {
                    case 'melvorD:Firemaking':
// We are currently at the topmost level of a skill object
                        importKeys = [
const gameSkill = game.skills.getObjectByID(dataNode.skillID);
                            'baseAshChance',
// For every skill with mastery, add mastery checkpoint descriptions
                            'baseStardustChance',
if (gameSkill instanceof SkillWithMastery && dataNode.data.masteryTokenID !== undefined && dataNode.data.masteryCheckpoints === undefined) {
                            'baseCharcoalChance'
const localID = this.getLocalID(dataNode.skillID);
                        ];
dataNode.data.baseMasteryPoolCap = gameSkill.baseMasteryPoolCap;
                        break;
dataNode.data.masteryCheckpoints = [];
                    case 'melvorD:Mining':
masteryCheckpoints.forEach((pct, idx) => {
                        importKeys = [
dataNode.data.masteryCheckpoints[idx] = this.getLangString('MASTERY_CHECKPOINT', `${ localID }_${ idx }`);
                            'baseInterval',
});
                            'baseRockHP',
}
                            'passiveRegenInterval'
if (!this.skillDataInit[dataNode.skillID]) {
                        ];
if (gameSkill !== undefined) {
                        dataNode.baseGemChance = 1;
// Import other attributes varying by skill
                        break;
let importKeys = [];
                    case 'melvorD:Smithing':
switch(dataNode.skillID) {
                    case 'melvorD:Fletching':
case 'melvorD:Firemaking':
                    case 'melvorD:Crafting':
importKeys = [
                    case 'melvorD:Runecrafting':
'baseAshChance',
                    case 'melvorD:Herblore':
'baseStardustChance',
                        importKeys = [
'baseCharcoalChance'
                            'baseInterval'
];
                        ];
break;
                        break;
case 'melvorD:Mining':
                    case 'melvorD:Thieving':
importKeys = [
                        importKeys = [
'baseInterval',
                            'baseInterval',
'baseRockHP',
                            'baseStunInterval',
'passiveRegenInterval'
                            'itemChance',
];
                            'baseAreaUniqueChance'
dataNode.data.baseGemChance = 1;
                        ];
dataNode.data.rockTypes = loadedLangJson.MINING_TYPE;
                        break;
break;
                    case 'melvorD:Agility':
case 'melvorD:Smithing':
                        importKeys = [
case 'melvorD:Fletching':
                            'obstacleUnlockLevels'
case 'melvorD:Crafting':
                        ];
case 'melvorD:Runecrafting':
                        break;
case 'melvorD:Herblore':
                    case 'melvorD:Summoning':
importKeys = [
                        importKeys = [
'baseInterval'
                            'baseInterval'
];
                        ];
break;
                        const sumKeys = [
case 'melvorD:Thieving':
                            'recipeGPCost',
importKeys = [
                            'markLevels'   
'baseInterval',
                        ];
'baseStunInterval',
                        sumKeys.forEach((k) => dataNode.data[k] = Summoning[k]);
'itemChance',
                        break;
'baseAreaUniqueChance'
                    case 'melvorD:Astrology':
];
                        // Astrology has a number of values stored outside of gameSkill
break;
                        const astKeys = [
case 'melvorD:Agility':
                            'standardModifierLevels',
importKeys = [
                            'uniqueModifierLevels',
'obstacleUnlockLevels'
                            'standardModifierCosts',
];
                            'uniqueModifierCosts',
break;
                            'baseStardustChance',
case 'melvorD:Summoning':
                            'baseGoldenStardustChance',
importKeys = [
                            'baseInterval'
'baseInterval'
                        ];
];
                        astKeys.forEach((k) => dataNode.data[k] = Astrology[k]);
const sumKeys = [
                        break;
'recipeGPCost',
                    case 'melvorD:Township':
'markLevels'   
                        // Remap a number of keys from their in-game names
];
                        const townKeys = [
sumKeys.forEach((k) => dataNode.data[k] = Summoning[k]);
                            {'from': 'TICK_LENGTH', 'to': 'tickLength'},
break;
                            {'from': 'MAX_TOWN_SIZE', 'to': 'maxTownSize'},
case 'melvorD:Astrology':
                            {'from': 'SECTION_SIZE', 'to': 'sectionSize'},
// Astrology has a number of values stored outside of gameSkill
                            {'from': 'INITIAL_CITIZEN_COUNT', 'to': 'initialCitizenCount'},
const astKeys = [
                            {'from': 'MIN_WORKER_AGE', 'to': 'minWorkerAge'},
'standardModifierLevels',
                            {'from': 'MAX_WORKER_AGE', 'to': 'maxWorkerAge'},
'uniqueModifierLevels',
                            {'from': 'AGE_OF_DEATH', 'to': 'ageOfDeath'},
'standardModifierCosts',
                            {'from': 'MIN_MIGRATION_AGE', 'to': 'minMigrationAge'},
'uniqueModifierCosts',
                            {'from': 'MAX_MIGRATION_AGE', 'to': 'maxMigrationAge'},
'baseStardustChance',
                            {'from': 'BASE_TAX_RATE', 'to': 'baseTaxRate'},
'baseGoldenStardustChance',
                            {'from': 'EDUCATION_PER_CITIZEN', 'to': 'educationPerCitizen'},
'baseInterval'
                            {'from': 'HAPPINESS_PER_CITIZEN', 'to': 'happinessPerCitizen'},
];
                            {'from': 'CITIZEN_FOOD_USAGE', 'to': 'citizenFoodUsage'},
astKeys.forEach((k) => dataNode.data[k] = Astrology[k]);
                            {'from': 'POPULATION_REQUIRED_FOR_BIRTH', 'to': 'populationRequiredForBirth'},
break;
                            {'from': 'BASE_STORAGE', 'to': 'baseStorage'},
case 'melvorD:Township':
                            {'from': 'WORSHIP_CHECKPOINTS', 'to': 'worshipCheckpoints'},
// Remap a number of keys from their in-game names
                            {'from': 'MAX_WORSHIP', 'to': 'maxWorship'},
const townKeys = [
                            {'from': 'populationForTier', 'to': 'populationForTier'},
{from: 'BASE_STORAGE', to: 'baseStorage'},
                            {'from': 'MAX_TRADER_STOCK_INCREASE', 'to': 'maxTraderStockIncrease'},
{from: 'BASE_TAX_RATE', to: 'baseTaxRate'},
                        ];
{from: 'DECREASED_BUILDING_COST_CAP', to: 'decreasedBuildingCostCap' },
                        townKeys.forEach((k) => dataNode.data[k.to] = gameSkill[k.from]);
{from: 'GP_PER_CITIZEN', to: 'gpPerCitizen'},
                        break;
{from: 'MAX_WORSHIP', to: 'maxWorship'},
                }
{from: 'MINIMUM_HEALTH', to: 'minimumHealth'},
                if (importKeys.length > 0) {
{from: 'populationForTier', to: 'populationForTier'},
                    importKeys.forEach((k) => dataNode.data[k] = gameSkill[k]);
{from: 'TICK_LENGTH', to: 'tickLength'},
                }
{from: 'RARE_SEASON_CHANCE', to: 'rareSeasonChance'},
            }
{from: 'WORSHIP_CHANGE_COST', to: 'worshipChangeCost'},
            this.skillDataInit[dataNode.skillID] = true;
{from: 'WORSHIP_CHECKPOINTS', to: 'worshipCheckpoints'},
        }
];
    }
townKeys.forEach((k) => dataNode.data[k.to] = gameSkill[k.from]);
    registerDataPackage(namespace) {
// Add task categories & localization of name
        // Consolidates the data package identified by namespace with existing data within
const taskCategories = Array.from(new Set(gameSkill.tasks.tasks.allObjects.map((t) => t.category)));
        // this.gameData
dataNode.data.taskCategories = taskCategories.map((i) => ({ id: i, name: gameSkill.tasks.getTownshipTaskCategoryName(i)}));
        const packData = this.packData[namespace].data;
break;
        if (packData === undefined) {
}
            throw new Error(`Couldn't find data for package ${ namespace }`);
if (importKeys.length > 0) {
        }
importKeys.forEach((k) => dataNode.data[k] = gameSkill[k]);
        // Add data within the game but outside of data packs
}
        this.registerNonPackData();
}
        // Consolidate data
this.skillDataInit[dataNode.skillID] = true;
        Object.keys(packData).forEach((categoryName) => {
}
            let categoryData = packData[categoryName];
// Appy localization (skills)
            // Some data is adjusted before combining - do this here
this.langApply(parentNode, nodeKey, true);
            if (['combatAreas', 'dungeons', 'slayerAreas'].includes(categoryName)) {
}
                // Add area type to each area object
}
                const areaTypes = {
registerDataPackage(namespace) {
                    'combatAreas': 'combatArea',
// Consolidates the data package identified by namespace with existing data within
                    'dungeons': 'dungeon',
// this.gameData
                    'slayerAreas': 'slayerArea'
const packData = this.packData[namespace].data;
                }
if (packData === undefined) {
                const areaType = areaTypes[categoryName];
throw new Error(`Couldn't find data for package ${ namespace }`);
                const newData = structuredClone(categoryData);
}
                newData.forEach((x) => x.type = areaType);
// Add data within the game but outside of data packs
                categoryData = newData;
this.registerNonPackData();
            }
// Consolidate data
            else if (['ancientSpells', 'archaicSpells', 'auroraSpells', 'curseSpells', 'standardSpells'].includes(categoryName)) {
Object.keys(packData).forEach((categoryName) => {
                // For spell books, add the spell type to each spell object.
let categoryData = packData[categoryName];
                // Alt Magic spells are handled elsewhere, as they are within a skill object
// Some data is adjusted before combining - do this here
                const spellType = categoryName.replace('Spells', '');
if (['combatAreas', 'dungeons', 'slayerAreas'].includes(categoryName)) {
                const newData = structuredClone(categoryData);
// Add area type to each area object
                newData.forEach((x) => x.spellBook = spellType);
const areaTypes = {
                categoryData = newData;
'combatAreas': 'combatArea',
            }
'dungeons': 'dungeon',
            else if (categoryName === 'golbinRaid') {
'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
// Data must be pushed into the consoldiated data, rules for vary
            // depending on the category in question
// depending on the category in question
            switch(categoryName) {
switch(categoryName) {
                case 'ancientSpells':
case 'ancientRelics':
                case 'archaicSpells':
case 'ancientSpells':
                case 'attackStyles':
case 'archaicSpells':
                case 'attacks':
case 'attackStyles':
                case 'auroraSpells':
case 'attacks':
                case 'combatAreas':
case 'auroraSpells':
                case 'combatEvents':
case 'combatAreas':
                case 'combatPassives':
case 'combatEvents':
                case 'curseSpells':
case 'combatPassives':
                case 'dungeons':
case 'curseSpells':
                case 'gamemodes':
case 'dungeons':
                case 'itemEffects':
case 'gamemodes':
                case 'itemSynergies':
case 'itemEffects':
                case 'itemUpgrades':
case 'itemSynergies':
                case 'itmMonsters':
case 'itemUpgrades':
                case 'items':
case 'itmMonsters':
                case 'lore':
case 'items':
                case 'monsters':
case 'lore':
                case 'pages':
case 'monsters':
                case 'pets':
case 'pages':
                case 'prayers':
case 'pets':
                case 'randomGems':
case 'prayers':
                case 'randomSuperiorGems':
case 'randomGems':
                case 'shopCategories':
case 'randomSuperiorGems':
                case 'shopPurchases':
case 'shopCategories':
                case 'shopUpgradeChains':
case 'shopPurchases':
                case 'slayerAreas':
case 'shopUpgradeChains':
                case 'stackingEffects':
case 'slayerAreas':
                case 'standardSpells':
case 'stackingEffects':
                case 'steamAchievements':
case 'standardSpells':
                case 'tutorialStages':
case 'steamAchievements':
                case 'spiderLairMonsters':
case 'tutorialStages':
                    // Plain old push to the end of the array
case 'spiderLairMonsters':
                    if (this.gameData[categoryName] === undefined) {
// Plain old push to the end of the array
                        // Category doesn't exist yet in consolidated data, so create it
if (this.gameData[categoryName] === undefined) {
                        this.gameData[categoryName] = categoryData;
// Category doesn't exist yet in consolidated data, so create it
                    }
this.gameData[categoryName] = categoryData;
                    else {
}
                        this.gameData[categoryName].push(...categoryData);
else {
                    }
this.gameData[categoryName].push(...categoryData);
                    break;
}
                case 'combatAreaDisplayOrder':
break;
                case 'dungeonDisplayOrder':
case 'bankSortOrder':
                case 'shopCategoryOrder':
case 'combatAreaDisplayOrder':
                case 'shopDisplayOrder':
case 'dungeonDisplayOrder':
                case 'slayerAreaDisplayOrder':
case 'shopCategoryOrder':
                case 'tutorialStageOrder':
case 'shopDisplayOrder':
                    // Elements are inserted at a particular index, controlled by rules
case 'slayerAreaDisplayOrder':
                    // specified within the data package
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;
break;
                case 'golbinRaid':
case 'golbinRaid':
                    // Properties contain unordered arrays that need to be combined
// Properties contain unordered arrays that need to be combined
                    if (this.gameData[categoryName] === undefined) {
if (this.gameData[categoryName] === undefined) {
                        this.gameData[categoryName] = categoryData;
this.gameData[categoryName] = categoryData;
                        this.gameData.golbinRaid.possibleModifiers = RaidManager.possibleModifiers;
this.gameData.golbinRaid.possibleModifiers = RaidManager.possibleModifiers;
                    }
}
                    else {
else {
                        Object.keys(categoryData).forEach((dataKey) => {
Object.keys(categoryData).forEach((dataKey) => {
                            if ((this.gameData[categoryName][dataKey] === undefined) || !Array.isArray(this.gameData[categoryName][dataKey])) {
if ((this.gameData[categoryName][dataKey] === undefined) || !Array.isArray(this.gameData[categoryName][dataKey])) {
                                // Property is undefined or isn't an array
// Property is undefined or isn't an array
                                this.gameData[categoryName][dataKey] = categoryData[dataKey];
this.gameData[categoryName][dataKey] = categoryData[dataKey];
                            }  
}  
                            else {
else {
                                // Property is an array
// Property is an array
                                this.gameData[categoryName][dataKey].push(...categoryData[dataKey]);
this.gameData[categoryName][dataKey].push(...categoryData[dataKey]);
                            }
}
                        });
});
                    }
}
                    break;
break;
                case 'skillData':
case 'skillData':
                    // Contains nested objects
// Contains nested objects
                    if (this.gameData[categoryName] === undefined) {
if (this.gameData[categoryName] === undefined) {
                        this.gameData[categoryName] = [];
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 492: Line 481:
const skillObj = this.gameData[categoryName][skillIdx].data;
const skillObj = this.gameData[categoryName][skillIdx].data;
Object.keys(skillData.data).forEach((dataKey) => {
Object.keys(skillData.data).forEach((dataKey) => {
if (Array.isArray(skillData.data[dataKey]) && skillData.data[dataKey].length > 0 && skillData.data[dataKey][0].insertAt !== undefined) {
// Special case for Township item conversions
//Data is ordered, special handling applies
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]);
skillObj[dataKey] = this.combineOrderedData(skillObj[dataKey], skillData.data[dataKey]);
}
}
Line 506: Line 510:
});
});
});
});
                    break;
break;
                default:
default:
                    console.warn(`Skipping unknown category while registering data package: ${ categoryName }`);
console.warn(`Skipping unknown category while registering data package: ${ categoryName }`);
                    break;
break;
            }
}
        });
});
    }
// If the data package contains modifications, apply these also
    registerNonPackData() {
const modificationData = this.packData[namespace].modifications;
        // Some data resides outside of packages. Add any such data to this.gameData within this function
if (modificationData !== undefined) {
        if (this.gameData.namespaces === undefined) {
this.applyDataModifications(modificationData);
            const nsData = [];
}
            game.registeredNamespaces.forEach((ns) => {
const dependentData = this.packData[namespace].dependentData;
                if (ns.isModded) {
if (dependentData !== undefined) {
                    throw new Error(`Modded namespace '${ ns.displayName }' found, all mods must be disabled before game data can be generated`);
// TODO Handle dependentData
                }
}
                else {
}
                    nsData.push(ns);
applyDataModifications(modData) {
                }
const modDataKeys = Object.keys(modData)
            });
for (const modCatID in modDataKeys) {
            this.gameData.namespaces = nsData;
const modCat = modDataKeys[modCatID];
        }
const catData = modData[modCat];
        if (this.gameData.combatTriangles === undefined) {
if ((modCat === 'shopUpgradeChains') || (modCat === 'shopPurchases')) {
            const ctData = [];
// Modify the root upgrade ID of shop upgrade chains, and modify attributes of shop purchases
            Object.keys(COMBAT_TRIANGLE_IDS).forEach((id) => {
catData.forEach((modItem) => {
                const newObj = structuredClone(combatTriangle[COMBAT_TRIANGLE_IDS[id]]);
const modObjID = modItem.id;
                newObj.id = id;
if (modObjID === undefined) {
                ctData.push(newObj);
console.warn(`Could not apply data modification: ID of object to be modified not found, category "${ modCat }"`);
            });
}
            this.gameData.combatTriangles = ctData;
else {
        }
const modObj = this.getObjectByID(this.gameData[modCat], modObjID);
        if (this.gameData.masteryCheckpoints === undefined) {
if (modObj === undefined) {
            this.gameData.masteryCheckpoints = masteryCheckpoints;
console.warn(`Could not apply data modification: Object with ID "${ modObjID }" not found for category "${ modCat }"`);
        }
}
        if (this.gameData.combatAreaDifficulties === undefined) {
else {
            this.gameData.combatAreaDifficulties = CombatAreaMenu.difficulty.map((i) => i.name);
const overrideKeys = {
        }
purchaseRequirements: {
        if (this.gameData.equipmentSlots === undefined) {
sourceKey: 'newRequirements', // Key that holds the data in the data package
            this.gameData.equipmentSlots = EquipmentSlots;
destKey: 'purchaseRequirementsOverrides', // Key to insert into within this.gameData
        }
subKey: 'requirements' // Sub-key containing the override data
        if (this.gameData.attackTypes === undefined) {
},
            this.gameData.attackTypes = AttackTypeID;
cost: {
        }  
sourceKey: 'newCosts',
        if (this.gameData.slayerTiers === undefined) {
destKey: 'costOverrides',
            const newData = structuredClone(SlayerTask.data)
subKey: 'cost'
            newData.forEach((tier) => delete tier.engDisplay);
}
            this.gameData.slayerTiers = newData;
};
        }
Object.keys(modItem).filter((k) => k !== 'id').forEach((k) => {
    }
const overrideKey = overrideKeys[k];
if (overrideKey !== undefined) {
// Is an override specific to a gamemode, do not replace
// the key's existing data
const destKey = overrideKey.destKey;
if (modObj[destKey] === undefined) {
modObj[destKey] = [];
}
modItem[k].forEach((gamemodeOverride) => {
var newData = {};
newData.gamemodeID = gamemodeOverride.gamemodeID;
newData[overrideKey.subKey] = gamemodeOverride[overrideKey.sourceKey];
modObj[destKey].push(newData);
});
}
else {
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 if (modCat === 'dungeons') {
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.dungeons, 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 === 'gamemodeRewardItemIDs') {
// Add gamemode specific item rewards to dungeon data
const itemRules = modItem[k];
Object.keys(itemRules).forEach((ruleKey) => {
if (ruleKey === 'add') {
if (modObj[k] === undefined) {
modObj[k] = [];
}
itemRules[ruleKey].forEach((itemDef) => {
let gamemodeRewards = this.getObjectByID(modObj[k], itemDef.gamemodeID, 'gamemodeID');
if (gamemodeRewards === undefined) {
modObj[k].push(({
gamemodeID: itemDef.gamemodeID,
itemIDs: itemDef.rewardItemIDs
}));
}
else {
gamemodeRewards.push(...itemDef.rewardItemIDs);
}
});
}
else {
console.warn(`Could not apply data modification: Unknown rule for gamemode item rewards: "${ ruleKey }", object "${ modObjID }"`);
}
});
}
else if (k === 'gamemodeEntryRequirements') {
// Add or remove gamemode specific entry requirements to dungeon data
if (modObj[k] === undefined) {
modObj[k] = [];
}
modObj[k].push(modItem[k]);
}
else {
console.warn(`Could not apply data modification: Unhandled key "${ k }" for category "${ modCat }", object "${ modObjID }"`);
}
});
}
}
});
}
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 = CombatAreaMenuElement.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) {
combineOrderedData(existingData, newData) {
// Elements are inserted at a particular index, controlled by rules
// Elements are inserted at a particular index, controlled by rules
Line 577: Line 797:
throw new Error(`Couldn't insert before: Item ${ orderData.beforeID } is not in the array.`);
throw new Error(`Couldn't insert before: Item ${ orderData.beforeID } is not in the array.`);
}
}
resultData.splice(beforeIndex, 0, ...orderData.ids);
resultData.splice(beforeIdx, 0, ...orderData.ids);
break;
break;
case 'After':
case 'After':
Line 590: Line 810:
return resultData;
return resultData;
}
}
    getNamespacedID(namespace, ID) {
// Determines if properties of entities are to be removed, as they are unused in the wiki
        if (ID.indexOf(':') > 0) {
// and would otherwise bloat the data.
            return ID;
// Returns true if the property is to be removed, false if it is to be retained
        }
isPropertyFiltered(entityType, entity, propertyName) {
        else {
switch(propertyName) {
            return namespace + ':' + ID;
case 'media':
        }
case 'altMedia':
    }
case 'markMedia':
    getLocalID(ID) {
case 'icon':
        if (ID.indexOf(':') > 0) {
case 'barStyle': // See: melvorD:Compost
            return ID.split(':').pop();
case 'buttonStyle':
        }
case 'descriptionGenerator':
        else {
case 'containerID':
            return ID;
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 relicDesc = (data) => {
const relic = game.ancientRelics.getObjectByID(data.id);
if (relic !== undefined) {
return relic.name;
}
}
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 = {
ancientRelics: {
name: { stringSpecial: 'relicDesc' }
},
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}' }
}
},
Archaeology: {
digSites: {
name: { key: 'POI_NAME_Melvor' }
}
// TODO Tool names
},
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}' }
}
},
Cartography: {
mapPortals: { _handler: 'mapPortals' },
travelEvents: {
description: { key: 'TRAVEL_EVENT' }
},
worldMaps: { _handler: 'cartoMaps' }
//name: { key: 'WORLD_MAP_NAME' },
//pointsOfInterest: { _handler: 'mapPOI' }
//name: { key: 'POI_NAME', idFormat: '{MAPID}_{ID}' },
//description: { key: 'POI_DESCRIPTION', idFormat: '{MAPID}_{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) => {
const handlerFunc = langKeyData[langKey]['_handler'];
if (handlerFunc !== undefined) {
switch(handlerFunc) {
case 'mapPortals':
Object.keys(target).forEach((portalKey) => {
let portalData = target[portalKey];
const langID = this.getLocalID(portalData.originWorldMap) + '_' + this.getLocalID(portalData.id);
portalData.name = this.getLangString('POI_NAME', langID);
portalData.description = this.getLangString('POI_DESCRIPTION', langID);
});
break;
case 'cartoMaps':
// Target represents a world map
const mapID = this.getLocalID(target.id);
target.name = this.getLangString('WORLD_MAP_NAME', mapID);
// Process POIs
target.pointsOfInterest.forEach((poi) => {
const langID = mapID + '_' + this.getLocalID(poi.id);
poi.name = this.getLangString('POI_NAME', langID);
poi.description = this.getLangString('POI_DESCRIPTION', langID);
});
break;
}
}
else {
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 'relicDesc':
langString = relicDesc(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) {
if (identifier === undefined) {
return loadedLangJson[key];
}
else {
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;
let wd = new Wiki;
wd.printWikiData();</pre>}}
wd.printWikiData();</syntaxhighlight>}}