Module:GameData/doc: Difference between revisions

Update code for localization & various other bits
(Add MAX_TRADER_STOCK_INCREASE to townKeys for next update)
(Update code for localization & various other bits)
Line 9: Line 9:
{{SpoilerBox|color=default|title=Code|text=<pre>// TODO:
{{SpoilerBox|color=default|title=Code|text=<pre>// TODO:
// Handle modifications portion of data packages
// Handle modifications portion of data packages
// Use actual descriptions as per language data
class Wiki {
class Wiki {
    constructor() {
constructor() {
        this.namespaces = {
this.debugMode = false;
            melvorD: { displayName: "Demo", url: "https://" + location.hostname + "/assets/data/melvorDemo.json" },
this.prettyPrint = false;
            melvorF: { displayName: "Full Version", url: "https://" + location.hostname + "/assets/data/melvorFull.json" },
this.namespaces = {
            melvorTotH: { displayName: "Throne of the Herald", url: "https://" + location.hostname + "/assets/data/melvorTotH.json" }
melvorD: { displayName: "Demo", url: "https://" + location.hostname + "/assets/data/melvorDemo.json" },
        };
melvorF: { displayName: "Full Version", url: "https://" + location.hostname + "/assets/data/melvorFull.json" },
        // Check all required namespaces are registered, as there are still some bits of data extracted from in-game rather than the data packages
melvorTotH: { displayName: "Throne of the Herald", url: "https://" + location.hostname + "/assets/data/melvorTotH.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.`);
}
});


        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;
async printWikiData() {
                }
if (!isLoaded) {
            }
throw new Error('Game must be loaded into a character first');
        };
}
        this.dataPropTransforms = {
if (Object.keys(this.packData).length < Object.keys(this.namespaces).length) {
            // Specifies rules for transforming values of entity properties
// Need to retrieve game data first
            // Format: property: ruleFunction(entityType, entity)
const result = await this.getWikiData();
            //      where ruleFunction is a function returning the transformed value
}
            //      to be used in place of the original value
let dataObjText;
            langHint: function(ns, entityType, entity) {
if (this.prettyPrint) {
                let localID = ''
dataObjText = JSON.stringify(this.gameData, undefined, '\t');
                if (entity.id.indexOf(':') > 0) {
}
                    localID = entity.id.split(':').pop();
else {
                }
dataObjText = JSON.stringify(this.gameData);
                else {
}
                    localID = entity.id
dataObjText = dataObjText.replace(/\'/g, "\\\'");
                }
dataObjText = dataObjText.replace(/\\\"/g, "\\\\\"");
                return getLangString(entity.category, localID);
            },
            equipmentStats: function(ns, entityType, entity) {
                const newStats = {};
                entity.forEach((stat) => {
                    if (newStats[stat.key] === undefined) {
                        newStats[stat.key] = stat.value;
                    }
                    else {
                        newStats[stat.key] += stat.value;
                    }
                });
                return newStats;
            },
            altSpells: function(ns, entityType, entity) {
                if (entityType !== 'skillData') {
                    return entity;
                }
                else {
                    const newData = structuredClone(entity);
                    newData.forEach((i) => {
                        i.spellBook = 'altMagic';
                    });
                    return newData;
                }
            },
            attacks: function(ns, entityType, entity) {
                if (entityType !== 'attacks') {
                    return entity;
                }
                else {
                    entity.forEach((attDef) => {
                        const nsAttID = ns + ':' + attDef.id;
                        const att = game.specialAttacks.getObjectByID(nsAttID);
                        attDef.description = att.description;
                    });
                    return entity;
                }
            }
        };
        this.dataPropTransforms.langCustomDescription = this.dataPropTransforms.langHint;
    };
    async getWikiData() {
        if (!isLoaded) {
            throw new Error('Game must be loaded into a character first');
        }
        for (const nsIdx in Object.keys(this.namespaces)) {
            const ns = Object.keys(this.namespaces)[nsIdx];
            const dataURL = this.namespaces[ns].url;
            console.log(`URL: ${ dataURL }`);
            const dataPackage = await this.getDataPackage(dataURL);
            if (dataPackage.namespace === undefined) {
                throw new Error(`Data package has no namespace: ${ dataURL }`);
            }
            else if (dataPackage.data === undefined) {
                throw new Error(`Data package has no data: ${ dataURL }`);
            }
            console.log(`Obtained data for namespace ${ dataPackage.namespace }, ${ JSON.stringify(dataPackage.data).length.toLocaleString() } bytes`);
            this.processDataPackage(dataPackage);
            console.log(`After transformation: ${ JSON.stringify(dataPackage.data).length.toLocaleString() } bytes`);
        }
        // All data packages should now be within this.gameData
    }
    getGameVersion() {
        const fileDOM = document.querySelector('#sidebar ul.nav-main');
        let fileVer = "Unknown";
        if (fileDOM !== null && fileDOM.dataset !== undefined) {
            fileVer = fileDOM.dataset.fileVersion;
        }
        return gameVersion + ' (' + fileVer + ')';
    }
    async printWikiData() {
        if (!isLoaded) {
            throw new Error('Game must be loaded into a character first');
        }
        if (Object.keys(this.packData).length < Object.keys(this.namespaces).length) {
            // Need to retrieve game data first
            const result = await this.getWikiData();
        }
        let dataObjText = JSON.stringify(this.gameData);
        dataObjText = dataObjText.replace(/\'/g, "\\\'");
        dataObjText = dataObjText.replace(/\\\"/g, "\\\\\"");


        let dataText = '-- Version: ' + this.getGameVersion();
let dataText = '-- Version: ' + this.getGameVersion();
        dataText += "\r\n\r\nlocal gameData = mw.text.jsonDecode('";
dataText += "\r\n\r\nlocal gameData = mw.text.jsonDecode('";
        dataText += dataObjText;
dataText += dataObjText;
        dataText += "')\r\n\r\nreturn gameData";
dataText += "')\r\n\r\nreturn gameData";
        console.log(dataText);
console.log(dataText);
    }
}
    async getDataPackage(url) {
async getDataPackage(url) {
        // Based largely on Game.fetchAndRegisterDataPackage()
// Based largely on Game.fetchAndRegisterDataPackage()
        const headers = new Headers();
const headers = new Headers();
        headers.append('Content-Type', 'application/json');
headers.append('Content-Type', 'application/json');
        return await fetch(url, {
return await fetch(url, {
            method: 'GET',
method: 'GET',
            headers
headers
        }).then(function(response) {
}).then(function(response) {
            if (!response.ok) {
if (!response.ok) {
                throw new Error(`Couldn't fetch data package from URL: ${ url }`);
throw new Error(`Couldn't fetch data package from URL: ${ url }`);
            }
}
            return response.json();
return response.json();
        });
});
    }
}
    processDataPackage(dataPackage) {
processDataPackage(dataPackage) {
        // Transforms the raw data from data packages in various ways, then
// Transforms the raw data from data packages in various ways, then
        // consolidates into this.packData & this.gameData
// consolidates into this.packData & this.gameData
        const ns = dataPackage.namespace;
const ns = dataPackage.namespace;
        const packData = dataPackage.data;
const packData = dataPackage.data;


        this.transformDataPackage(dataPackage);
this.transformDataPackage(dataPackage);
        this.packData[dataPackage.namespace] = dataPackage;
this.packData[dataPackage.namespace] = dataPackage;
        this.registerDataPackage(dataPackage.namespace);
this.registerDataPackage(dataPackage.namespace);
    }
}
    transformDataPackage(dataPackage) {
transformDataPackage(dataPackage) {
        // Takes a raw data package and performs various manipulations
// Takes a raw data package and performs various manipulations
        const ns = dataPackage.namespace;
const ns = dataPackage.namespace;
        const packData = dataPackage.data;
const packData = dataPackage.data;


        Object.keys(packData).forEach((categoryName) => {
Object.keys(packData).forEach((categoryName) => {
            switch(categoryName) {
switch(categoryName) {
                case 'bankSortOrder':
case 'bankSortOrder':
                case 'steamAchievements':
case 'pages':
                    // This data serves no purpose for the wiki and only serves to bloat
case 'steamAchievements':
                    // the data, so simply delete it
case 'tutorialStageOrder':
                    delete packData[categoryName];
case 'tutorialStages':
                    break;
// This data serves no purpose for the wiki and only serves to bloat
                default:
// the data, so simply delete it
                    this.transformDataNode(ns, categoryName, packData, categoryName);
delete packData[categoryName];
                    break;
break;
            }
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) {
transformDataNode(ns, categoryName, parentNode, nodeKey) {
            // A transformation function is defined for this node
let dataNode = parentNode[nodeKey];
            parentNode[nodeKey] = transformFunc(ns, categoryName, dataNode);
const transformedValue = this.transformProperty(categoryName, dataNode, nodeKey, ns);
            dataNode = parentNode[nodeKey];
if (transformedValue !== undefined) {
        }
// A transformed value exists for this node
        if (Array.isArray(dataNode)) {
parentNode[nodeKey] = transformedValue;
            // Recursive call to ensure all data is transformed, regardless of its depth
dataNode = parentNode[nodeKey];
            dataNode.forEach((entity, idx) => this.transformDataNode(ns, categoryName, dataNode, idx));
}
        }
if (Array.isArray(dataNode)) {
        else if (typeof dataNode === 'object' && dataNode !== null) {
// Recursive call to ensure all data is transformed, regardless of its depth
            // Iterate properties of object, checking if each should be deleted or transformed
dataNode.forEach((entity, idx) => this.transformDataNode(ns, categoryName, dataNode, idx));
            Object.keys(dataNode).forEach((key) => {
}
                // Check if property is to be deleted or not
else if (typeof dataNode === 'object' && dataNode !== null) {
                const filterFunc = this.dataPropFilters[key];
// Iterate properties of object, checking if each should be deleted or transformed
                if (filterFunc !== undefined && !filterFunc(categoryName, dataNode)) {
Object.keys(dataNode).forEach((key) => {
                    delete dataNode[key];
// Check if property is to be deleted or not
                }
if (this.isPropertyFiltered(categoryName, dataNode, key)) {
                else if (typeof dataNode[key] === "object" && dataNode[key] !== null) {
delete dataNode[key];
                    // If an object (either an array or key/value store) is within the current
}
                    // object then we must traverse this too
else if (typeof dataNode[key] === "object" && dataNode[key] !== null) {
                    this.transformDataNode(ns, categoryName, dataNode, key);
// If an object (either an array or key/value store) is within the current
                }
// object then we must traverse this too
                else {
this.transformDataNode(ns, categoryName, dataNode, key);
                    // Transform property, if a transformation is defined below
}
                    switch(key) {
else {
                        case 'id':
// Transform property, if a transformation is defined below
                            // Add namespace to ID if it isn't already
switch(key) {
                            dataNode[key] = this.getNamespacedID(ns, dataNode[key]);
case 'id':
                            break;
// Add namespace to ID if it isn't already
                        case 'name':
dataNode[key] = this.getNamespacedID(ns, dataNode[key]);
                            // Some items have underscores in their names, replace with spaces
break;
                            if (categoryName == 'items') {
}
                                dataNode[key] = dataNode[key].replaceAll('_', ' ');
}
                            }
});
                            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 && !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) {
// Special case for skillData so that certain values initialized when the various Skill
                    dataNode.data.milestoneCount = gameSkill.milestones.length;
// classes are initialized may be added here also
                }
if ((categoryName === 'skillData') && dataNode.skillID !== undefined && dataNode.data !== undefined) {
                // For every skill with mastery, add mastery checkpoint descriptions
// We are currently at the topmost level of a skill object
                if (gameSkill instanceof SkillWithMastery && dataNode.data.masteryTokenID !== undefined && dataNode.data.masteryCheckpoints === undefined) {
if (!this.skillDataInit[dataNode.skillID]) {
                    const localID = this.getLocalID(dataNode.skillID);
const gameSkill = game.skills.getObjectByID(dataNode.skillID);
                    dataNode.data.baseMasteryPoolCap = gameSkill.baseMasteryPoolCap;
if (gameSkill !== undefined) {
                    dataNode.data.masteryCheckpoints = [];
if (gameSkill.milestones !== undefined && dataNode.data.milestoneCount === undefined) {
                    masteryCheckpoints.forEach((pct, idx) => {
dataNode.data.milestoneCount = gameSkill.milestones.length;
                        dataNode.data.masteryCheckpoints[idx] = getLangString('MASTERY_CHECKPOINT', `${ localID }_${ idx }`);
}
                    });
// For every skill with mastery, add mastery checkpoint descriptions
                }
if (gameSkill instanceof SkillWithMastery && dataNode.data.masteryTokenID !== undefined && dataNode.data.masteryCheckpoints === undefined) {
const localID = this.getLocalID(dataNode.skillID);
dataNode.data.baseMasteryPoolCap = gameSkill.baseMasteryPoolCap;
dataNode.data.masteryCheckpoints = [];
masteryCheckpoints.forEach((pct, idx) => {
dataNode.data.masteryCheckpoints[idx] = getLangString('MASTERY_CHECKPOINT', `${ localID }_${ idx }`);
});
}


                // Import other attributes varying by skill
// Import other attributes varying by skill
                let importKeys = [];
let importKeys = [];
                switch(dataNode.skillID) {
switch(dataNode.skillID) {
                    case 'melvorD:Firemaking':
case 'melvorD:Firemaking':
                        importKeys = [
importKeys = [
                            'baseAshChance',
'baseAshChance',
                            'baseStardustChance',
'baseStardustChance',
                            'baseCharcoalChance'
'baseCharcoalChance'
                        ];
];
                        break;
break;
                    case 'melvorD:Mining':
case 'melvorD:Mining':
                        importKeys = [
importKeys = [
                            'baseInterval',
'baseInterval',
                            'baseRockHP',
'baseRockHP',
                            'passiveRegenInterval'
'passiveRegenInterval'
                        ];
];
                        dataNode.baseGemChance = 1;
dataNode.data.baseGemChance = 1;
                        break;
dataNode.data.rockTypes = loadedLangJson.MINING_TYPE;
                    case 'melvorD:Smithing':
break;
                    case 'melvorD:Fletching':
case 'melvorD:Smithing':
                    case 'melvorD:Crafting':
case 'melvorD:Fletching':
                    case 'melvorD:Runecrafting':
case 'melvorD:Crafting':
                    case 'melvorD:Herblore':
case 'melvorD:Runecrafting':
                        importKeys = [
case 'melvorD:Herblore':
                            'baseInterval'
importKeys = [
                        ];
'baseInterval'
                        break;
];
                    case 'melvorD:Thieving':
break;
                        importKeys = [
case 'melvorD:Thieving':
                            'baseInterval',
importKeys = [
                            'baseStunInterval',
'baseInterval',
                            'itemChance',
'baseStunInterval',
                            'baseAreaUniqueChance'
'itemChance',
                        ];
'baseAreaUniqueChance'
                        break;
];
                    case 'melvorD:Agility':
break;
                        importKeys = [
case 'melvorD:Agility':
                            'obstacleUnlockLevels'
importKeys = [
                        ];
'obstacleUnlockLevels'
                        break;
];
                    case 'melvorD:Summoning':
break;
                        importKeys = [
case 'melvorD:Summoning':
                            'baseInterval'
importKeys = [
                        ];
'baseInterval'
                        const sumKeys = [
];
                            'recipeGPCost',
const sumKeys = [
                            'markLevels'   
'recipeGPCost',
                        ];
'markLevels'   
                        sumKeys.forEach((k) => dataNode.data[k] = Summoning[k]);
];
                        break;
sumKeys.forEach((k) => dataNode.data[k] = Summoning[k]);
                    case 'melvorD:Astrology':
break;
                        // Astrology has a number of values stored outside of gameSkill
case 'melvorD:Astrology':
                        const astKeys = [
// Astrology has a number of values stored outside of gameSkill
                            'standardModifierLevels',
const astKeys = [
                            'uniqueModifierLevels',
'standardModifierLevels',
                            'standardModifierCosts',
'uniqueModifierLevels',
                            'uniqueModifierCosts',
'standardModifierCosts',
                            'baseStardustChance',
'uniqueModifierCosts',
                            'baseGoldenStardustChance',
'baseStardustChance',
                            'baseInterval'
'baseGoldenStardustChance',
                        ];
'baseInterval'
                        astKeys.forEach((k) => dataNode.data[k] = Astrology[k]);
];
                        break;
astKeys.forEach((k) => dataNode.data[k] = Astrology[k]);
                    case 'melvorD:Township':
break;
                        // Remap a number of keys from their in-game names
case 'melvorD:Township':
                        const townKeys = [
// Remap a number of keys from their in-game names
                            {'from': 'TICK_LENGTH', 'to': 'tickLength'},
const townKeys = [
                            {'from': 'MAX_TOWN_SIZE', 'to': 'maxTownSize'},
{'from': 'TICK_LENGTH', 'to': 'tickLength'},
                            {'from': 'SECTION_SIZE', 'to': 'sectionSize'},
{'from': 'MAX_TOWN_SIZE', 'to': 'maxTownSize'},
                            {'from': 'INITIAL_CITIZEN_COUNT', 'to': 'initialCitizenCount'},
{'from': 'SECTION_SIZE', 'to': 'sectionSize'},
                            {'from': 'MIN_WORKER_AGE', 'to': 'minWorkerAge'},
{'from': 'INITIAL_CITIZEN_COUNT', 'to': 'initialCitizenCount'},
                            {'from': 'MAX_WORKER_AGE', 'to': 'maxWorkerAge'},
{'from': 'MIN_WORKER_AGE', 'to': 'minWorkerAge'},
                            {'from': 'AGE_OF_DEATH', 'to': 'ageOfDeath'},
{'from': 'MAX_WORKER_AGE', 'to': 'maxWorkerAge'},
                            {'from': 'MIN_MIGRATION_AGE', 'to': 'minMigrationAge'},
{'from': 'AGE_OF_DEATH', 'to': 'ageOfDeath'},
                            {'from': 'MAX_MIGRATION_AGE', 'to': 'maxMigrationAge'},
{'from': 'MIN_MIGRATION_AGE', 'to': 'minMigrationAge'},
                            {'from': 'BASE_TAX_RATE', 'to': 'baseTaxRate'},
{'from': 'MAX_MIGRATION_AGE', 'to': 'maxMigrationAge'},
                            {'from': 'EDUCATION_PER_CITIZEN', 'to': 'educationPerCitizen'},
{'from': 'BASE_TAX_RATE', 'to': 'baseTaxRate'},
                            {'from': 'HAPPINESS_PER_CITIZEN', 'to': 'happinessPerCitizen'},
{'from': 'EDUCATION_PER_CITIZEN', 'to': 'educationPerCitizen'},
                            {'from': 'CITIZEN_FOOD_USAGE', 'to': 'citizenFoodUsage'},
{'from': 'HAPPINESS_PER_CITIZEN', 'to': 'happinessPerCitizen'},
                            {'from': 'POPULATION_REQUIRED_FOR_BIRTH', 'to': 'populationRequiredForBirth'},
{'from': 'CITIZEN_FOOD_USAGE', 'to': 'citizenFoodUsage'},
                            {'from': 'BASE_STORAGE', 'to': 'baseStorage'},
{'from': 'POPULATION_REQUIRED_FOR_BIRTH', 'to': 'populationRequiredForBirth'},
                            {'from': 'WORSHIP_CHECKPOINTS', 'to': 'worshipCheckpoints'},
{'from': 'BASE_STORAGE', 'to': 'baseStorage'},
                            {'from': 'MAX_WORSHIP', 'to': 'maxWorship'},
{'from': 'WORSHIP_CHECKPOINTS', 'to': 'worshipCheckpoints'},
                            {'from': 'populationForTier', 'to': 'populationForTier'},
{'from': 'MAX_WORSHIP', 'to': 'maxWorship'},
                            {'from': 'MAX_TRADER_STOCK_INCREASE', 'to': 'maxTraderStockIncrease'},
{'from': 'populationForTier', 'to': 'populationForTier'},
                        ];
{'from': 'MAX_TRADER_STOCK_INCREASE', 'to': 'maxTraderStockIncrease'},
                        townKeys.forEach((k) => dataNode.data[k.to] = gameSkill[k.from]);
];
                        break;
townKeys.forEach((k) => dataNode.data[k.to] = gameSkill[k.from]);
                }
// Add task categories & localization of name
                if (importKeys.length > 0) {
const taskCategories = Array.from(new Set(gameSkill.tasks.tasks.allObjects.map((t) => t.category)));
                    importKeys.forEach((k) => dataNode.data[k] = gameSkill[k]);
dataNode.data.taskCategories = taskCategories.map((i) => ({ id: i, name: gameSkill.tasks.getTownshipTaskCategoryName(i)}));
                }
break;
            }
}
            this.skillDataInit[dataNode.skillID] = true;
if (importKeys.length > 0) {
        }
importKeys.forEach((k) => dataNode.data[k] = gameSkill[k]);
    }
}
    registerDataPackage(namespace) {
}
        // Consolidates the data package identified by namespace with existing data within
this.skillDataInit[dataNode.skillID] = true;
        // this.gameData
}
        const packData = this.packData[namespace].data;
// Appy localization (skills)
        if (packData === undefined) {
this.langApply(parentNode, nodeKey, true);
            throw new Error(`Couldn't find data for package ${ namespace }`);
}
        }
}
        // Add data within the game but outside of data packs
registerDataPackage(namespace) {
        this.registerNonPackData();
// Consolidates the data package identified by namespace with existing data within
        // Consolidate data
// this.gameData
        Object.keys(packData).forEach((categoryName) => {
const packData = this.packData[namespace].data;
            let categoryData = packData[categoryName];
if (packData === undefined) {
            // Some data is adjusted before combining - do this here
throw new Error(`Couldn't find data for package ${ namespace }`);
            if (['combatAreas', 'dungeons', 'slayerAreas'].includes(categoryName)) {
}
                // Add area type to each area object
// Add data within the game but outside of data packs
                const areaTypes = {
this.registerNonPackData();
                    'combatAreas': 'combatArea',
// Consolidate data
                    'dungeons': 'dungeon',
Object.keys(packData).forEach((categoryName) => {
                    'slayerAreas': 'slayerArea'
let categoryData = packData[categoryName];
                }
// Some data is adjusted before combining - do this here
                const areaType = areaTypes[categoryName];
if (['combatAreas', 'dungeons', 'slayerAreas'].includes(categoryName)) {
                const newData = structuredClone(categoryData);
// Add area type to each area object
                newData.forEach((x) => x.type = areaType);
const areaTypes = {
                categoryData = newData;
'combatAreas': 'combatArea',
            }
'dungeons': 'dungeon',
            else if (['ancientSpells', 'archaicSpells', 'auroraSpells', 'curseSpells', 'standardSpells'].includes(categoryName)) {
'slayerAreas': 'slayerArea'
                // 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 areaType = areaTypes[categoryName];
                const spellType = categoryName.replace('Spells', '');
const newData = structuredClone(categoryData);
                const newData = structuredClone(categoryData);
newData.forEach((x) => x.type = areaType);
                newData.forEach((x) => x.spellBook = spellType);
categoryData = newData;
                categoryData = newData;
}
            }
else if (['ancientSpells', 'archaicSpells', 'auroraSpells', 'curseSpells', 'standardSpells'].includes(categoryName)) {
            else if (categoryName === 'golbinRaid') {
// 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 'ancientSpells':
                case 'archaicSpells':
case 'archaicSpells':
                case 'attackStyles':
case 'attackStyles':
                case 'attacks':
case 'attacks':
                case 'auroraSpells':
case 'auroraSpells':
                case 'combatAreas':
case 'combatAreas':
                case 'combatEvents':
case 'combatEvents':
                case 'combatPassives':
case 'combatPassives':
                case 'curseSpells':
case 'curseSpells':
                case 'dungeons':
case 'dungeons':
                case 'gamemodes':
case 'gamemodes':
                case 'itemEffects':
case 'itemEffects':
                case 'itemSynergies':
case 'itemSynergies':
                case 'itemUpgrades':
case 'itemUpgrades':
                case 'itmMonsters':
case 'itmMonsters':
                case 'items':
case 'items':
                case 'lore':
case 'lore':
                case 'monsters':
case 'monsters':
                case 'pages':
case 'pages':
                case 'pets':
case 'pets':
                case 'prayers':
case 'prayers':
                case 'randomGems':
case 'randomGems':
                case 'randomSuperiorGems':
case 'randomSuperiorGems':
                case 'shopCategories':
case 'shopCategories':
                case 'shopPurchases':
case 'shopPurchases':
                case 'shopUpgradeChains':
case 'shopUpgradeChains':
                case 'slayerAreas':
case 'slayerAreas':
                case 'stackingEffects':
case 'stackingEffects':
                case 'standardSpells':
case 'standardSpells':
                case 'steamAchievements':
case 'steamAchievements':
                case 'tutorialStages':
case 'tutorialStages':
                case 'spiderLairMonsters':
case 'spiderLairMonsters':
                    // Plain old push to the end of the array
// Plain old push to the end of the array
                    if (this.gameData[categoryName] === undefined) {
if (this.gameData[categoryName] === undefined) {
                        // Category doesn't exist yet in consolidated data, so create it
// Category doesn't exist yet in consolidated data, so create it
                        this.gameData[categoryName] = categoryData;
this.gameData[categoryName] = categoryData;
                    }
}
                    else {
else {
                        this.gameData[categoryName].push(...categoryData);
this.gameData[categoryName].push(...categoryData);
                    }
}
                    break;
break;
                case 'combatAreaDisplayOrder':
case 'combatAreaDisplayOrder':
                case 'dungeonDisplayOrder':
case 'dungeonDisplayOrder':
                case 'shopCategoryOrder':
case 'shopCategoryOrder':
                case 'shopDisplayOrder':
case 'shopDisplayOrder':
                case 'slayerAreaDisplayOrder':
case 'slayerAreaDisplayOrder':
                case 'tutorialStageOrder':
case 'tutorialStageOrder':
                    // Elements are inserted at a particular index, controlled by rules
// Elements are inserted at a particular index, controlled by rules
                    // specified within the data package
// 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 506: Line 432:
});
});
});
});
                    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;
            }
}
        });
});
    }
}
    registerNonPackData() {
registerNonPackData() {
        // Some data resides outside of packages. Add any such data to this.gameData within this function
// Some data resides outside of packages. Add any such data to this.gameData within this function
        if (this.gameData.namespaces === undefined) {
if (this.gameData.namespaces === undefined) {
            const nsData = [];
const nsData = [];
            game.registeredNamespaces.forEach((ns) => {
game.registeredNamespaces.forEach((ns) => {
                if (ns.isModded) {
if (ns.isModded) {
                    throw new Error(`Modded namespace '${ ns.displayName }' found, all mods must be disabled before game data can be generated`);
throw new Error(`Modded namespace '${ ns.displayName }' found, all mods must be disabled before game data can be generated`);
                }
}
                else {
else {
                    nsData.push(ns);
nsData.push(ns);
                }
}
            });
});
            this.gameData.namespaces = nsData;
this.gameData.namespaces = nsData;
        }
}
        if (this.gameData.combatTriangles === undefined) {
if (this.gameData.combatTriangles === undefined) {
            const ctData = [];
const ctData = [];
            Object.keys(COMBAT_TRIANGLE_IDS).forEach((id) => {
Object.keys(COMBAT_TRIANGLE_IDS).forEach((id) => {
                const newObj = structuredClone(combatTriangle[COMBAT_TRIANGLE_IDS[id]]);
const newObj = structuredClone(combatTriangle[COMBAT_TRIANGLE_IDS[id]]);
                newObj.id = id;
newObj.id = id;
                ctData.push(newObj);
ctData.push(newObj);
            });
});
            this.gameData.combatTriangles = ctData;
this.gameData.combatTriangles = ctData;
        }
}
        if (this.gameData.masteryCheckpoints === undefined) {
if (this.gameData.masteryCheckpoints === undefined) {
            this.gameData.masteryCheckpoints = masteryCheckpoints;
this.gameData.masteryCheckpoints = masteryCheckpoints;
        }
}
        if (this.gameData.combatAreaDifficulties === undefined) {
if (this.gameData.combatAreaDifficulties === undefined) {
            this.gameData.combatAreaDifficulties = CombatAreaMenu.difficulty.map((i) => i.name);
this.gameData.combatAreaDifficulties = CombatAreaMenu.difficulty.map((i) => i.name);
        }
}
        if (this.gameData.equipmentSlots === undefined) {
if (this.gameData.equipmentSlots === undefined) {
            this.gameData.equipmentSlots = EquipmentSlots;
//TODO: Amend to follow { id: ..., name: ... } structure. Obtain name from getLangString('EQUIP_SLOT', numID)
        }
this.gameData.equipmentSlots = EquipmentSlots;
        if (this.gameData.attackTypes === undefined) {
}
            this.gameData.attackTypes = AttackTypeID;
if (this.gameData.attackTypes === undefined) {
        }  
this.gameData.attackTypes = AttackTypeID;
        if (this.gameData.slayerTiers === undefined) {
}  
            const newData = structuredClone(SlayerTask.data)
if (this.gameData.slayerTiers === undefined) {
            newData.forEach((tier) => delete tier.engDisplay);
const newData = structuredClone(SlayerTask.data)
            this.gameData.slayerTiers = newData;
newData.forEach((tier) => delete tier.engDisplay);
        }
this.gameData.slayerTiers = newData;
    }
}
}
combineOrderedData(existingData, newData) {
combineOrderedData(existingData, newData) {
// Elements are inserted at a particular index, controlled by rules
// Elements are inserted at a particular index, controlled by rules
Line 590: Line 517:
return resultData;
return resultData;
}
}
    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 getLangString(entity.category, this.getLocalID(entity.id));
case 'equipmentStats':
const newStats = {};
entity.forEach((stat) => {
if (newStats[stat.key] === undefined) {
newStats[stat.key] = stat.value;
}
else {
newStats[stat.key] += stat.value;
}
});
return newStats;
case 'altSpells':
if (entityType !== 'skillData') {
return undefined;
}
else {
const newData = structuredClone(entity);
newData.forEach((i) => {
i.spellBook = 'altMagic';
});
return newData;
}
default:
return undefined;
}
}
langApply(parentNode, nodeKey, isSkill) {
const nodeName = (isSkill ? parentNode[nodeKey].skillID : nodeKey);
const altMagicDescIDKey = function(data) {
// Accepts an Alt. Magic spell object, returns the ID format for that spell
// Using a function for this as some spells (e.g. Superheat) have bespoke logic
if (data.specialCost !== undefined && data.specialCost.type !== undefined) {
if (data.id.includes('HolyInvocation')) {
return 'HOLY_INVOCATION';
}
switch(data.specialCost.type) {
case 'BarIngredientsWithCoal':
return 'SUPERHEAT';
case 'BarIngredientsWithoutCoal':
return 'SUPERHEAT_NO_COAL';
case 'AnyItem':
if (data.produces !== undefined && data.produces === 'GP') {
return 'ITEM_ALCHEMY';
}
break;
}
}
return 'ALTMAGIC_DESC_{ID}';
};
const shopChainPropKey = function(data, dataKey, propName) {
// Accepts an upgrade chain data object & key of the property being localized
const propToLang = {
chainName: 'chainNameLang',
defaultDescription: 'descriptionLang',
defaultName: 'defaultNameLang'
};
const langPropName = propToLang[dataKey];
if (langPropName !== undefined) {
const langProp = data[langPropName];
if (langProp !== undefined) {
return langProp[propName];
}
}
}
const itemDesc = function(data) {
// Items have varying logic based on the type of item, and the lang data contains
// some incorrect stuff for items whose descriptions are generated entirely
// from modifiers, so just get the description from in-game objects instead.
let desc;
const item = game.items.getObjectByID(data.id);
if (item !== undefined) {
desc = item.description;
if (desc === getLangString('BANK_STRING', '38')) {
// Generic "no description" string
return undefined;
}
// Temporary fix for issue with language data keys for FrostSpark 1H Sword
else if (desc.includes('UNDEFINED TRANSLATION') && data.id === 'melvorTotH:FrostSpark_1H_Sword') {
return getLangString('ITEM_DESCRIPTION', 'Frostspark_1H_Sword')
}
else {
return desc;
}
}
}
const passiveDesc = function(data) {
const passive = game.combatPassives.getObjectByID(data.id);
if (passive !== undefined) {
return passive.description;
}
}
const spAttDesc = function(data) {
const spAtt = game.specialAttacks.getObjectByID(data.id);
if (spAtt !== undefined) {
return spAtt.description;
}
}
const langKeys = {
ancientSpells: {
name: { key: 'MAGIC', idFormat: 'ANCIENT_NAME_{ID}' }
},
archaicSpells: {
name: { key: 'MAGIC', idFormat: 'ARCHAIC_NAME_{ID}' }
},
attackStyles: {
name: { key: 'COMBAT_MISC', idFormat: 'ATTACK_STYLE_NAME_{ID}' }
},
attacks: {
name: { key: 'SPECIAL_ATTACK_NAME' },
description: { stringSpecial: 'spAttDesc' }
},
auroraSpells: {
name: { key: 'MAGIC', idFormat: 'AURORA_NAME_{ID}' }
},
combatAreas: {
name: { key: 'COMBAT_AREA', idFormat: 'NAME_{ID}'}
},
combatPassives: {
name: { key: 'PASSIVES', idFormat: 'NAME_{ID}' },
customDescription: { stringSpecial: 'passiveDesc' }
//customDescription: { key: 'PASSIVES', idFormat: 'DESC_{ID}' }
},
curseSpells: {
name: { key: 'MAGIC', idFormat: 'CURSE_NAME_{ID}' }
},
dungeons: {
name: { key: 'DUNGEON', idFormat: 'NAME_{ID}' }
},
gamemodes: {
name: { key: 'GAMEMODES', idFormat: 'GAMEMODE_NAME_{ID}' },
description: { key: 'GAMEMODES', idFormat: 'GAMEMODE_DESC_{ID}' },
// Gamemodes have an array of rules
rules: { key: 'GAMEMODES', idFormat: 'GAMEMODE_RULES_{ID}_{NUM}' }
},
items: {
name: { key: 'ITEM_NAME' },
customDescription: { stringSpecial: 'itemDesc', onlyIfExists: true }
},
lore: {
title: { key: 'LORE', idFormat: 'TITLE_{ID}' }
},
monsters: {
name: { key: 'MONSTER_NAME' },
description: { key: 'MONSTER_DESCRIPTION' }
},
pets: {
name: { key: 'PET_NAME' }
},
prayers: {
name: { key: 'PRAYER', idFormat: 'PRAYER_NAME_{ID}' }
},
shopCategories: {
name: { key: 'SHOP_CAT' }
},
shopPurchases: {
customName: { key: 'SHOP_NAME', onlyIfExists: true },
customDescription: { key: 'SHOP_DESCRIPTION', onlyIfExists: true }
},
shopUpgradeChains: {
chainName: { keySpecial: 'shopChainKey', idSpecial: 'shopChainID' },
defaultDescription: { keySpecial: 'shopChainKey', idSpecial: 'shopChainID' },
defaultName: { keySpecial: 'shopChainKey', idSpecial: 'shopChainID' }
 
},
slayerAreas: {
name: { key: 'SLAYER_AREA', idFormat: 'NAME_{ID}' },
areaEffectDescription: { key: 'SLAYER_AREA', idFormat: 'EFFECT_{ID}' }
},
standardSpells: {
name: { key: 'MAGIC', idFormat: 'SPELL_NAME_{ID}' }
},
skillData: {
// Each skill is nested within this, so follow much the same structure
// Keys here are each skill's local ID
_common: {
// Special entry, contains lang definitions which are the same
// for all skills
_root: {
name: { key: 'SKILL_NAME', idFormat: '{SKILLID}' }
},
categories: {
name: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}' }
},
customMilestones: {
name: { key: 'MILESTONES', idKey: 'milestoneID' }
},
masteryLevelUnlocks: {
description: { key: 'MASTERY_BONUS', idKey: 'descriptionID', idFormat: '{SKILLID}_{ID}' }
}
},
Agility: {
elitePillars: {
name: { key: 'AGILITY', idFormat: 'PILLAR_NAME_{ID}' }
},
obstacles: {
name: { key: 'AGILITY', idFormat: 'OBSTACLE_NAME_{ID}' }
},
pillars: {
name: { key: 'AGILITY', idFormat: 'PILLAR_NAME_{ID}' }
}
},
Astrology: {
recipes: {
name: { key: 'ASTROLOGY', idFormat: 'NAME_{ID}' }
}
},
Farming: {
categories: {
description: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_description' },
seedNotice: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_seedNotice' },
singularName: { key: 'SKILL_CATEGORY', idFormat: '{SKILLID}_{ID}_singular' }
}
},
Fishing: {
areas: {
name: { key: 'FISHING', idFormat: 'AREA_NAME_{ID}' }
}
},
Herblore: {
recipes: {
name: { key: 'POTION_NAME' }
},
Magic: {
altSpells: {
name: { key: 'MAGIC', idFormat: 'ALTMAGIC_NAME_{ID}' },
description: { key: 'MAGIC', idSpecial: 'altMagicDesc' }
}
},
Mining: {
rockData: {
name: { key: 'ORE_NAME' }
}
},
Summoning: {
synergies: {
customDescription: { key: 'SUMMONING_SYNERGY', idKey: 'summonIDs', idFormat: 'DESC_{ID0}_{ID1}', onlyIfExists: true }
}
},
Thieving: {
areas: {
name: { key: 'THIEVING', idFormat: 'AREA_NAME_{ID}' }
},
npcs: {
name: { key: 'THIEVING', idFormat: 'NPC_NAME_{ID}' }
}
},
Township: {
biomes: {
// Can't locate biome description localization, don't think this is exposed in game UI
name: { key: 'TOWNSHIP', idFormat: 'BIOME_{ID}' }
},
buildings: {
// Building description has no localization, as it is unused
name: { key: 'TOWNSHIP', idFormat: 'BUILDING_{ID}' }
},
jobs: {
name: { key: 'TOWNSHIP', idFormat: 'JOB_{ID}' }
},
resources: {
name: { key: 'TOWNSHIP', idFormat: 'RESOURCE_{ID}' }
},
tasks: {
// name is not exposed in game UI, and has no localization
// category is localized in transformDataNode
description: { key: 'TOWNSHIP_TASKS', idFormat: '{ID}_description' }
},
worships: {
name: { key: 'MONSTER_NAME' },
statueName: { key: 'TOWNSHIP', idFormat: 'Statue_of_{ID}' }
}
},
Woodcutting: {
trees: {
name: { key: 'TREE_NAME' }
}
}
}
};
 
// Determine which language key data applies
var langKeyData;
if (isSkill) {
// Combine common & skill specific keys
const skillKey = this.getLocalID(parentNode[nodeKey].skillID);
const langCommon = langKeys.skillData._common;
let langSkill = structuredClone(langKeys.skillData[skillKey]);
 
if (langCommon !== undefined) {
if (langSkill === undefined) {
langSkill = {};
}
Object.keys(langCommon).forEach((k) => {
if (langSkill[k] === undefined) {
langSkill[k] = {};
}
Object.keys(langCommon[k]).forEach((prop) => {
langSkill[k][prop] = langCommon[k][prop];
});
});
}
langKeyData = langSkill;
}
else if (langKeys[nodeKey] !== undefined) {
langKeyData = { _root: langKeys[nodeKey] };
}
else {
console.warn('No lang key data found for ' + nodeKey);
}
 
if (langKeyData !== undefined) {
var dataToTranslate = parentNode[nodeKey];
if (isSkill) {
dataToTranslate = dataToTranslate.data;
}
if (!Array.isArray(dataToTranslate)) {
dataToTranslate = [ dataToTranslate ];
}
dataToTranslate.forEach((tData) => {
Object.keys(langKeyData).forEach((langKey) => {
const targetData = ((langKey === '_root') ? tData : tData[langKey]);
if (targetData !== undefined) {
const targetArr = (Array.isArray(targetData) ? targetData : [ targetData ]);
targetArr.forEach((target) => {
Object.keys(langKeyData[langKey]).forEach((langPropID) => {
const langProp = langKeyData[langKey][langPropID];
if (!langProp.onlyIfExists || target[langPropID] !== undefined) {
const langIDKey = langProp.idKey ?? 'id';
var langIDValue;
if (Array.isArray(target[langIDKey])) {
// The ID key can sometimes be an array of IDs (e.g. Summoning synergies)
langIDValue = target[langIDKey].map((id) => this.getLocalID((id ?? '').toString()));
}
else {
langIDValue = this.getLocalID((target[langIDKey] ?? '').toString());
}
let langIdent = langProp.idFormat;
if (langProp.idSpecial !== undefined) {
// Use a special method to determine the ID format
switch(langProp.idSpecial) {
case 'altMagicDesc':
langIdent = altMagicDescIDKey(target);
break;
case 'shopChainID':
langIdent = this.getLocalID(shopChainPropKey(target, langPropID, 'id'));
break;
}
}
if (langIdent === undefined) {
langIdent = langIDValue;
}
else {
// langIdent is in a specific format
const langTemplate = {}
if (isSkill) {
langTemplate.SKILLID = this.getLocalID(parentNode[nodeKey].skillID);
}
if (Array.isArray(langIDValue)) {
langIDValue.forEach((val, idx) => {
langTemplate['ID' + idx] = this.getLocalID(val);
});
}
else {
langTemplate.ID = langIDValue;
}
Object.keys(langTemplate).forEach((k) => {
langIdent = langIdent.replaceAll('{' + k + '}', langTemplate[k]);
});
}
 
let langCategoryKey = langProp.key;
if (langProp.keySpecial !== undefined) {
// Use a special method to determine the category key
switch(langProp.keySpecial) {
case 'shopChainKey':
langCategoryKey = shopChainPropKey(target, langPropID, 'category');
break;
}
}
 
if (Array.isArray(target[langPropID])) {
target[langPropID].forEach((targetElem, num) => {
const langIdentFinal = langIdent.replaceAll('{NUM}', num.toString());
const langString = this.getLangString(langCategoryKey, langIdentFinal);
target[langPropID][num] = langString;
if (this.debugMode) {
if (langString !== undefined) {
console.debug('Set value of property ' + langPropID + '[' + num.toString() + '] for ' + langIdentFinal + ' in node ' + nodeName + ' to: ' + langString);
}
else {
console.debug('No translation: property ' + langPropID + ' for ' + langIdentFinal + ' in node ' + nodeName);
}
}
});
}
else {
let langString;
if (langProp.stringSpecial !== undefined) {
// Use a custom function to determine the string
switch(langProp.stringSpecial) {
case 'itemDesc':
langString = itemDesc(target);
break;
case 'passiveDesc':
langString = passiveDesc(target);
break;
case 'spAttDesc':
langString = spAttDesc(target);
break;
}
}
else {
langString = this.getLangString(langCategoryKey, langIdent);
}
target[langPropID] = langString;
if (this.debugMode) {
if (langString !== undefined) {
console.debug('Set value of property ' + langPropID + ' for ' + langIdent + ' in node ' + nodeName + ' to: ' + langString);
}
else {
console.debug('No translation: property ' + langPropID + ' for ' + langIdent + ' in node ' + nodeName);
}
}
}
}
});
});
}
});
});
}
}
getLangString(key, identifier) {
const langCat = loadedLangJson[key];
if (langCat !== undefined) {
return langCat[identifier];
}
}
getNamespacedID(namespace, ID) {
if (ID.indexOf(':') > 0) {
return ID;
}
else {
return namespace + ':' + ID;
}
}
getLocalID(ID) {
if (ID.indexOf(':') > 0) {
return ID.split(':').pop();
}
else {
return ID;
}
}
}
}


let wd = new Wiki;
let wd = new Wiki;
wd.printWikiData();</pre>}}
wd.printWikiData();</pre>}}