Module:GameData/doc: Difference between revisions

Support all remaining data modifications & dependent data
(Add data version)
(Support all remaining data modifications & dependent data)
(6 intermediate revisions by the same user not shown)
Line 15: Line 15:
this.debugMode = false;
this.debugMode = false;
this.prettyPrint = false;
this.prettyPrint = false;
this.baseDir = '/assets/data/';
this.customLocalizations = {
// Contains custom localization strings, to override any game provided localizations.
// To be used sparingly, for instances where 2+ objects of the same type
// (e.g. monsters) have the same name, as this isn't convenient to deal with in Lua
 
// TotH curse also named 'Madness'
MAGIC_ABYSSAL_NAME_Madness: 'Madness (ItA)',
 
// Stronghold boss monsters, where names overlap with normal monster variants
MONSTER_NAME_FierceDevilBoss: 'Fierce Devil (Stronghold)',
MONSTER_NAME_ElementalistBoss: 'Elementalist (Stronghold)',
MONSTER_NAME_PratTheGuardianOfSecretsBoss: 'Prat, the Guardian of Secrets (Stronghold)',
MONSTER_NAME_MysteriousFigurePhase1Stronghold: 'Mysterious Figure - Phase 1 (Stronghold)',
MONSTER_NAME_MysteriousFigurePhase2Stronghold: 'Mysterious Figure - Phase 2 (Stronghold)',
MONSTER_NAME_AhreniaStronghold: 'Ahrenia (Stronghold)'
};
this.namespaces = {
this.namespaces = {
melvorD: { displayName: 'Demo', url: 'https://' + location.hostname + this.baseDir + 'melvorDemo.json' },
melvorD: {
melvorF: { displayName: 'Full Version', url: 'https://' + location.hostname + this.baseDir + 'melvorFull.json' },
displayName: 'Demo',
packFile: 'melvorDemo.json',
},
melvorF: {
displayName: 'Full Version',
packFile: 'melvorFull.json',
},
melvorTotH: {
melvorTotH: {
displayName: 'Throne of the Herald',
displayName: 'Throne of the Herald',
url: 'https://' + location.hostname + this.baseDir + 'melvorTotH.json',
packFile: 'melvorTotH.json',
},
},
melvorAoD: {
melvorAoD: {
displayName: 'Atlas of Discovery',
displayName: 'Atlas of Discovery',
url: 'https://' + location.hostname + this.baseDir + 'melvorExpansion2.json',
packFile: 'melvorExpansion2.json',
},
},
melvorBirthday2023: {
melvorBirthday2023: {
displayName: 'Melvor Birthday 2023',
displayName: 'Melvor Birthday 2023',
url: 'https://' + location.hostname + this.baseDir + 'melvorBirthday2023.json',
packFile: 'melvorBirthday2023.json',
},
},
melvorItA: {
melvorItA: {
displayName: 'Into the Abyss',
displayName: 'Into the Abyss',
url: 'https://' + location.hostname + this.baseDir + 'melvorItA.json',
packFile: 'melvorItA.json',
},
},
};
};
this.registeredNamespaces = [];
// List of categories to be excluded from the generated game data.
// These serve no purpose for the wiki and so would otherwise bloat the data
this.excludedCategories = [
'pages',
'steamAchievements',
'tutorialStageOrder',
'tutorialStages'
];
// Check all required namespaces are registered, as there are still some bits of data extracted from in-game rather than the data packages
// Check all required namespaces are registered, as there are still some bits of data extracted from in-game rather than the data packages
Object.keys(this.namespaces).forEach((nsID) => {
Object.keys(this.namespaces).forEach((nsID) => {
Line 85: Line 115:
this.gameData = {};
this.gameData = {};
this.skillDataInit = {};
this.skillDataInit = {};
}
getDataPackURL(nsID) {
return 'https://' + location.hostname + '/assets/data/' + this.namespaces[nsID].packFile + '?' + DATA_VERSION.toString();
}
}
async getWikiData() {
async getWikiData() {
Line 92: Line 125:
for (const nsIdx in Object.keys(this.namespaces)) {
for (const nsIdx in Object.keys(this.namespaces)) {
const ns = Object.keys(this.namespaces)[nsIdx];
const ns = Object.keys(this.namespaces)[nsIdx];
const dataURL = this.namespaces[ns].url;
const dataURL = this.getDataPackURL(ns);
console.log(`URL: ${dataURL}`);
console.log(`URL: ${dataURL}`);
const dataPackage = await this.getDataPackage(dataURL);
const dataPackage = await this.getDataPackage(dataURL);
Line 108: Line 141:
console.log(`After transformation: ${JSON.stringify(dataPackage.data).length.toLocaleString()} bytes`);
console.log(`After transformation: ${JSON.stringify(dataPackage.data).length.toLocaleString()} bytes`);
}
}
// Process dependent data after all packages processed
console.log('Processing dependent data for all packages...');
this.processDependentData();
// All data packages should now be within this.gameData
// All data packages should now be within this.gameData
}
}
Line 241: Line 277:
const packData = dataPackage.data;
const packData = dataPackage.data;


Object.keys(packData).forEach((categoryName) => {
Object.keys(packData)
switch (categoryName) {
.filter((categoryName) => !this.excludedCategories.includes(categoryName))
case 'pages':
.forEach((categoryName) => {
case 'steamAchievements':
this.transformDataNode(ns, categoryName, packData, categoryName);
case 'tutorialStageOrder':
});
case 'tutorialStages':
// This data serves no purpose for the wiki and only serves to bloat
// the data, so simply delete it
delete packData[categoryName];
break;
default:
this.transformDataNode(ns, categoryName, packData, categoryName);
break;
}
});
}
}
transformDataNode(ns, categoryName, parentNode, nodeKey) {
transformDataNode(ns, categoryName, parentNode, nodeKey) {
Line 344: Line 370:
case 'melvorD:Summoning':
case 'melvorD:Summoning':
importKeys = ['baseInterval'];
importKeys = ['baseInterval'];
const sumKeys = ['recipeGPCost', 'markLevels'];
const sumKeys = ['recipeAPCost', 'recipeGPCost', 'markLevels'];
sumKeys.forEach((k) => (dataNode.data[k] = Summoning[k]));
sumKeys.forEach((k) => (dataNode.data[k] = Summoning[k]));
break;
break;
Line 394: Line 420:
}
}
}
}
registerDataPackage(namespace) {
registerPackData(packData) {
// Consolidates the data package identified by namespace with existing data within
Object.keys(packData)
// this.gameData
.filter((categoryName) => !this.excludedCategories.includes(categoryName))
const packData = this.packData[namespace].data;
.forEach((categoryName) => {
if (packData === undefined) {
let categoryData = packData[categoryName];
throw new Error(`Couldn't find data for package ${namespace}`);
// Some data is adjusted before combining - do this here
}
if (['combatAreas', 'dungeons', 'slayerAreas', 'abyssDepths', 'strongholds'].includes(categoryName)) {
// Add data within the game but outside of data packs
// Add area type to each area object
this.registerNonPackData();
const areaTypes = {
// Consolidate data
Object.keys(packData).forEach((categoryName) => {
let categoryData = packData[categoryName];
// Some data is adjusted before combining - do this here
if (['combatAreas', 'dungeons', 'slayerAreas', 'abyssDepths'].includes(categoryName)) {
// Add area type to each area object
const areaTypes = {
combatAreas: 'combatArea',
combatAreas: 'combatArea',
dungeons: 'dungeon',
dungeons: 'dungeon',
slayerAreas: 'slayerArea',
slayerAreas: 'slayerArea',
strongholds: 'stronghold',
abyssDepths: 'abyssDepth',
abyssDepths: 'abyssDepth',
};
};
Line 419: Line 439:
newData.forEach((x) => (x.type = areaType));
newData.forEach((x) => (x.type = areaType));
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
Line 585: Line 597:
}
}
});
});
}
registerDataPackage(namespace) {
// Consolidates the data package identified by namespace with existing data within
// this.gameData
const packData = this.packData[namespace].data;
if (packData === undefined) {
throw new Error(`Couldn't find data for package ${namespace}`);
}
// Add data within the game but outside of data packs
this.registerNonPackData();
// Consolidate data
this.registerPackData(packData);
// If the data package contains modifications, apply these also
// If the data package contains modifications, apply these also
const modificationData = this.packData[namespace].modifications;
const modificationData = this.packData[namespace].modifications;
Line 590: Line 614:
this.applyDataModifications(modificationData);
this.applyDataModifications(modificationData);
}
}
const dependentData = this.packData[namespace].dependentData;
// Dependent data is handled later, once all packages have been registered
if (dependentData !== undefined) {
 
// TODO Handle dependentData
if (!this.registeredNamespaces.includes(namespace)) {
this.registeredNamespaces.push(namespace);
}
}
}
}
applyDataModifications(modData) {
processDependentData() {
// TODO: Handle modifications for the following:
Object.entries(this.packData)
// equipmentSlots - Currently tweaks passive slot requirements
.forEach(([namespace, packData]) => {
// pages - Not so important, this is unused data
if (packData.dependentData !== undefined) {
// damageTypes
packData.dependentData.forEach((depDataForNS) => {
// skillData - Adjusts Township season modifiers to include ItA resources
const depNS = depDataForNS.namespace;
const modDataKeys = Object.keys(modData);
if (!this.registeredNamespaces.includes(depNS)) {
for (const modCatID in modDataKeys) {
const modCat = modDataKeys[modCatID];
const catData = modData[modCat];
if (modCat === 'combatAreaCategories') {
// The 'areas' property of elements within the category data are ordered data
catData.forEach((modItem) => {
const modObjID = modItem.id;
if (modObjID === undefined) {
console.warn(
`Could not apply data modification: ID of object to be modified not found, category "${modCat}"`
);
} else {
const modObj = this.getObjectByID(this.gameData[modCat], modObjID);
if (modObj === undefined) {
console.warn(
console.warn(
`Could not apply data modification: Object with ID "${modObjID}" not found for category "${modCat}"`
`Could not apply dependent data from package ${namespace}: Data depends on namespace ${depNS}, which has not been registered`
);
);
} else {
modObj.areas = this.combineOrderedData(modObj.areas, modItem.areas.add);
}
}
}
else {
});
console.log(`Attempting to apply dependent data for ${depNS} from package ${namespace}`);
} else if (modCat === 'shopUpgradeChains' || modCat === 'shopPurchases') {
if (depDataForNS.data !== undefined) {
// Modify the root upgrade ID of shop upgrade chains, and modify attributes of shop purchases
this.registerPackData(depDataForNS.data)
catData.forEach((modItem) => {
}
const modObjID = modItem.id;
if (depDataForNS.modifications !== undefined) {
if (modObjID === undefined) {
this.applyDataModifications(depDataForNS.modifications);
console.warn(
}
`Could not apply data modification: ID of object to be modified not found, category "${modCat}"`
);
} else {
const modObj = this.getObjectByID(this.gameData[modCat], modObjID);
if (modObj === undefined) {
console.warn(
`Could not apply data modification: Object with ID "${modObjID}" not found for category "${modCat}"`
);
} else {
const overrideKeys = {
purchaseRequirements: {
sourceKey: 'newRequirements', // Key that holds the data in the data package
destKey: 'purchaseRequirementsOverrides', // Key to insert into within this.gameData
subKey: 'requirements', // Sub-key containing the override data
},
cost: {
sourceKey: 'newCosts',
destKey: 'costOverrides',
subKey: 'cost',
},
};
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) => {
getDataToModify(modCat) {
const modObjID = modItem.id;
switch (modCat) {
const cookingSkill = this.getObjectByID(this.gameData.skillData, 'melvorD:Cooking', 'skillID');
case 'combatAreaCategories':
if (modObjID === undefined) {
case 'damageTypes':
console.warn(
case 'dungeons':
`Could not apply data modification: ID of object to be modified not found, category "${modCat}"`
case 'equipmentSlots':
);
case 'gamemodes':
} else if (cookingSkill === undefined) {
case 'items':
console.warn('Could not apply data modification: Data for skill "melvorD:Cooking" not found');
case 'modifiers':
} else {
case 'pets':
const modObj = this.getObjectByID(cookingSkill.data.categories, modObjID);
case 'shopUpgradeChains':
if (modObj === undefined) {
case 'shopPurchases':
console.warn(
case 'skillData':
`Could not apply data modification: Object with ID "${modObjID}" not found for category "${modCat}"`
case 'slayerAreas':
);
return this.gameData[modCat];
} else {
case 'cookingCategories':
Object.keys(modItem)
const cookingSkill = this.getObjectByID(this.gameData.skillData, 'melvorD:Cooking', 'skillID');
.filter((k) => k !== 'id')
return cookingSkill.data.categories;
.forEach((k) => {
case 'fletchingRecipes':
if (k === 'shopUpgradeIDs') {
const fletchingSkill = this.getObjectByID(this.gameData.skillData, 'melvorD:Fletching', 'skillID');
if (modObj[k] === undefined) {
return fletchingSkill.data.recipes;
modObj[k] = modItem[k];
}
} else {
return undefined;
modObj[k].push(...modItem[k]);
}
}
applyModifierModifications(objToModify, adjustments) {
} else {
if (objToModify.modifiers === undefined) {
console.warn(
objToModify.modifiers = {};
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${mobObjID}"`
}
);
Object.keys(adjustments)
}
.forEach((adjType) => {
});
if (adjType === 'add') {
}
Object.entries(adjustments[adjType])
}
.forEach(([chgKey, chgVal]) => {
});
if (objToModify.modifiers[chgKey] === undefined) {
} else if (modCat === 'fletchingRecipes') {
objToModify.modifiers[chgKey] = chgVal;
// Append to alternativeCosts property of recipes (e.g. Arrow shafts)
}
catData.forEach((modItem) => {
else if (Array.isArray(chgVal)) {
const modObjID = modItem.id;
objToModify.modifiers[chgKey].push(...chgVal);
const fletchingSkill = this.getObjectByID(this.gameData.skillData, 'melvorD:Fletching', 'skillID');
}
if (modObjID === undefined) {
else {
console.warn(
objToModify.modifiers[chgKey] += chgVal;
`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 {
} else {
console.warn(
const modObj = this.getObjectByID(fletchingSkill.data.recipes, modObjID);
`Could not apply data modification: Unhandled modifier adjustment "${adjType}"`
if (modObj === undefined) {
);
console.warn(
}
`Could not apply data modification: Object with ID "${modObjID}" not found for category "${modCat}"`
}
);
);
} else {
}
Object.keys(modItem)
applyAddRemoveModifications(objToModify, adjustments, modifyKey) {
.filter((k) => k !== 'id')
if (adjustments.remove !== undefined && Array.isArray(objToModify[modifyKey])) {
.forEach((k) => {
// adjustments.remove is an array of requirement types to be removed
if (k === 'alternativeCosts') {
let i = 0;
if (modObj[k] === undefined) {
while (i < objToModify[modifyKey].length) {
modObj[k] = modItem[k];
if (adjustments.remove.includes(objToModify[modifyKey][i].type)) {
} else {
objToModify[modifyKey].splice(i, 1);
modObj[k].push(...modItem[k]);
}
}
else {
} else {
i++;
console.warn(
}
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${mobObjID}"`
}
);
}
}
if (adjustments.add !== undefined) {
});
if (objToModify[modifyKey] === undefined) {
}
objToModify[modifyKey] = adjustments.add;
}
}
});
else {
} else if (modCat === 'dungeons') {
objToModify[modifyKey].push(...adjustments.add);
}
}
}
applyGamemodeSpecificModifications(objToModify, adjustments, newProperty) {
const gamemodeID = adjustments.gamemodeID;
if (objToModify.gamemodeOverrides === undefined) {
objToModify.gamemodeOverrides = [];
}
let gamemodeEntryToModify = this.getObjectByID(objToModify.gamemodeOverrides, gamemodeID, 'gamemodeID');
if (gamemodeEntryToModify === undefined) {
// Initialize gamemode overrides
objToModify.gamemodeOverrides.push({
gamemodeID: gamemodeID
});
gamemodeEntryToModify = this.getObjectByID(objToModify.gamemodeOverrides, gamemodeID, 'gamemodeID');
}
if (gamemodeEntryToModify[newProperty] === undefined) {
gamemodeEntryToModify[newProperty] = structuredClone(objToModify[newProperty]) ?? {};
}
this.applyAddRemoveModifications(gamemodeEntryToModify, adjustments, newProperty);
}
applyDataModifications(modData) {
const modDataKeys = Object.keys(modData).filter((modCatID) => !this.excludedCategories.includes(modCatID));
for (const modCatID in modDataKeys) {
const modCat = modDataKeys[modCatID];
const catData = modData[modCat];
const dataToModify = this.getDataToModify(modCat);
const modObjIDKey = (modCat === 'skillData' ? 'skillID' : 'id');
if (dataToModify === undefined) {
console.warn(
`Could not apply data modification for category "${modCat}": Unable to retrieve category data to be modified`
);
}
else {
catData.forEach((modItem) => {
catData.forEach((modItem) => {
const modObjID = modItem.id;
const modObjID = modItem[modObjIDKey];
if (modObjID === undefined) {
if (modObjID === undefined) {
console.warn(
console.warn(
`Could not apply data modification: ID of object to be modified not found, category "${modCat}"`
`Could not apply data modification for category "${modCat}": ID of object to be modified not found`
);
);
} else {
} else {
const modObj = this.getObjectByID(this.gameData.dungeons, modObjID);
const objToModify = this.getObjectByID(dataToModify, modObjID, modObjIDKey);
if (modObj === undefined) {
if (objToModify === undefined) {
console.warn(
console.warn(
`Could not apply data modification: Object with ID "${modObjID}" not found for category "${modCat}"`
`Could not apply data modification: Object with ID "${modObjID}" not found for ctaegory "${modCat}"`
);
);
} else {
}
Object.keys(modItem)
else {
.filter((k) => k !== 'id')
switch (modCat) {
.forEach((k) => {
case 'combatAreaCategories':
if (k === 'gamemodeRewardItemIDs') {
// The 'areas' property of elements within the category data are ordered data
// Add gamemode specific item rewards to dungeon data
objToModify.areas = this.combineOrderedData(objToModify.areas, modItem.areas.add);
const itemRules = modItem[k];
break;
Object.keys(itemRules).forEach((ruleKey) => {
case 'damageTypes':
if (ruleKey === 'add') {
Object.entries(modItem)
if (modObj[k] === undefined) {
.filter(([k, v]) => k !== 'id')
modObj[k] = [];
.forEach(([k, v]) => {
}
if (typeof v === 'object' && (v.add !== undefined || v.remove !== undefined)) {
itemRules[ruleKey].forEach((itemDef) => {
this.applyAddRemoveModifications(objToModify, v, k);
let gamemodeRewards = this.getObjectByID(modObj[k], itemDef.gamemodeID, 'gamemodeID');
}
if (gamemodeRewards === undefined) {
else {
modObj[k].push({
gamemodeID: itemDef.gamemodeID,
itemIDs: itemDef.rewardItemIDs,
});
} else {
gamemodeRewards.push(...itemDef.rewardItemIDs);
}
});
} else {
console.warn(
console.warn(
`Could not apply data modification: Unknown rule for gamemode item rewards: "${ruleKey}", object "${modObjID}"`
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
);
}
}
});
});
} else if (k === 'gamemodeEntryRequirements') {
break;
// Add or remove gamemode specific entry requirements to dungeon data
case 'gamemodes':
if (modObj[k] === undefined) {
Object.entries(modItem)
modObj[k] = [];
.filter(([k, v]) => k !== 'id')
}
.forEach(([k, v]) => {
modObj[k].push(modItem[k]);
if (typeof v === 'object' && (v.add !== undefined || v.remove !== undefined)) {
} else {
this.applyAddRemoveModifications(objToModify, v, k);
console.warn(
}
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
else if (['abyssalLevelCapCost', 'post99RollConversion'].includes(k)) {
);
objToModify[k] = v;
}
}
});
else {
}
console.warn(
}
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
});
);
} else if (modCat === 'modifiers') {
}
catData.forEach((modItem) => {
});
const modObjID = modItem.id;
break;
if (modObjID === undefined) {
case 'shopPurchases':
console.warn(
case 'shopUpgradeChains':
`Could not apply data modification: ID of object to be modified not found, category "${modCat}"`);
// Modify the root upgrade ID of shop upgrade chains, and modify attributes of shop purchases
} else {
const overrideKeys = {
// Find modifier definition
purchaseRequirements: {
const modParentObj = this.getObjectByID(this.gameData.modifiers, modObjID);
sourceKey: 'newRequirements', // Key that holds the data in the data package
if ((modParentObj === undefined) || (modParentObj.allowedScopes === undefined)) {
destKey: 'purchaseRequirementsOverrides', // Key to insert into within this.gameData
console.warn(`Could not apply data modification: Modifier with ID ${modObjID} not found or modifier has no scopes`);
subKey: 'requirements', // Sub-key containing the override data
} else {
},
modItem.allowedScopes.forEach((srcScope) => {
cost: {
// Find scope within modifier modParentObj with matching scopes definition
sourceKey: 'newCosts',
const srcScopeKeys = Object.keys(srcScope.scopes);
destKey: 'costOverrides',
modParentObj.allowedScopes.forEach((destScope) => {
subKey: 'cost',
const destScopeKeys = Object.keys(destScope.scopes);
},
const scopeMatch = (
};
srcScopeKeys.length === destScopeKeys.length
Object.keys(modItem)
&& srcScopeKeys.every((k) => destScope.scopes[k] !== undefined && srcScope.scopes[k] == destScope.scopes[k])
.filter((k) => k !== 'id')
);
.forEach((k) => {
if (scopeMatch) {
const overrideKey = overrideKeys[k];
// Scopes match - add aliases to modifier allowedScope definition
if (overrideKey !== undefined) {
const aliasKeys = ['posAliases', 'negAliases'];
// Is an override specific to a gamemode, do not replace
aliasKeys.forEach((aliasKey) => {
// the key's existing data
if (srcScope[aliasKey] !== undefined) {
const destKey = overrideKey.destKey;
if (destScope[aliasKey] === undefined) {
if (objToModify[destKey] === undefined) {
destScope[aliasKey] = [];
objToModify[destKey] = [];
}
}
destScope[aliasKey].push(...srcScope[aliasKey]);
modItem[k].forEach((gamemodeOverride) => {
var newData = {};
newData.gamemodeID = gamemodeOverride.gamemodeID;
newData[overrideKey.subKey] = gamemodeOverride[overrideKey.sourceKey];
objToModify[destKey].push(newData);
});
} else {
objToModify[k] = modItem[k];
}
}
});
});
}
break;
});
case 'cookingCategories':
});
// Append to the list of shop upgrade IDs for cooking utilities/categories
}
case 'fletchingRecipes':
}
// Append to alternativeCosts property of recipes (e.g. Arrow shafts)
});
Object.keys(modItem)
} else {
.filter((k) => k !== 'id')
console.warn(`Could not apply data modification: Unhandled category "${modCat}"`);
.forEach((k) => {
}
if ((k === 'shopUpgradeIDs') || (k === 'alternativeCosts')) {
}
if (objToModify[k] === undefined) {
}
objToModify[k] = modItem[k];
registerNonPackData() {
} else {
// Some data resides outside of packages. Add any such data to this.gameData within this function
objToModify[k].push(...modItem[k]);
// Metadata for data/file version
}
if (this.gameData._dataVersion === undefined) {
} else {
this.gameData._dataVersion = ({
console.warn(
gameVersion: this.getGameVersion().substring(1),
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
fileVersion: this.getGameFileVersion().substring(1)
);
});
}
}
});
// Namespaces
break;
if (this.gameData.namespaces === undefined) {
case 'skillData':
const nsData = [];
Object.entries(modItem.data)
game.registeredNamespaces.forEach((ns) => {
.forEach(([skillProp, propModData]) => {
if (ns.isModded) {
propModData.forEach((subModItem) => {
throw new Error(
const subObjToModify = this.getObjectByID(objToModify.data[skillProp], subModItem.id);
`Modded namespace '${ns.displayName}' found, all mods must be disabled before game data can be generated`
if (subObjToModify === undefined) {
);
console.warn(`Couldn't find skill object with ID ${subModItem.id} to modify. Property ${skillProp} in skill ID ${objToModify.skillID}`);
} else {
}
nsData.push(ns);
else {
}
Object.entries(subModItem)
});
.forEach(([subProp, subData]) => {
this.gameData.namespaces = nsData;
if (subProp === 'modifiers') {
}
this.applyModifierModifications(subObjToModify, subData);
if (this.gameData.currencies === undefined) {
}
this.gameData.currencies = game.currencies.allObjects.map((c) => ({
else if (subProp !== 'id') {
id: c.id,
this.applyAddRemoveModifications(subObjToModify, subData, subProp);
name: c.name,
}
type: c.type
});
}));
}
}
});
// Melvor realm exists outside of data packages
});
if (this.gameData.realms === undefined) {
break;
this.gameData.realms = game.realms
case 'dungeons':
.filter((r) => r.id === 'melvorD:Melvor')
// Add gamemode specific data to dungeons
.map((r) => ({
Object.keys(modItem)
id: r.id,
.filter((k) => k !== 'id')
name: r.name,
.forEach((k) => {
unlockRequirements: r.unlockRequirements
if (k === 'gamemodeRewardItemIDs') {
}));
// Add gamemode specific item rewards to dungeon data
const itemRules = modItem[k];
Object.keys(itemRules).forEach((ruleKey) => {
if (ruleKey === 'add') {
itemRules[ruleKey].forEach((itemDef) => {
const modToApply = {
gamemodeID: itemDef.gamemodeID,
add: itemDef.rewardItemIDs
}
this.applyGamemodeSpecificModifications(objToModify, modToApply, '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
this.applyGamemodeSpecificModifications(objToModify, modItem[k], 'entryRequirements');
} else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
});
break;
case 'modifiers':
// Add modifier aliases to existing mod scopes
if (objToModify.allowedScopes === undefined) {
console.warn(`Could not apply data modification: Modifier with ID ${modObjID} not found or modifier has no scopes`);
} else {
modItem.allowedScopes.forEach((srcScope) => {
// Find scope within modifier objToModify with matching scopes definition
const srcScopeKeys = Object.keys(srcScope.scopes);
objToModify.allowedScopes.forEach((destScope) => {
const destScopeKeys = Object.keys(destScope.scopes);
const scopeMatch = (
srcScopeKeys.length === destScopeKeys.length
&& srcScopeKeys.every((k) => destScope.scopes[k] !== undefined && srcScope.scopes[k] == destScope.scopes[k])
);
if (scopeMatch) {
// Scopes match - add aliases to modifier allowedScope definition
const aliasKeys = ['posAliases', 'negAliases'];
aliasKeys.forEach((aliasKey) => {
if (srcScope[aliasKey] !== undefined) {
if (destScope[aliasKey] === undefined) {
destScope[aliasKey] = [];
}
destScope[aliasKey].push(...srcScope[aliasKey]);
}
});
}
});
});
}
break;
case 'items':
Object.keys(modItem)
.filter((k) => k !== 'id')
.forEach((k) => {
if (k === 'modifiers') {
this.applyModifierModifications(objToModify, modItem[k]);
}
else if (k === 'consumesOn') {
Object.keys(modItem[k])
.forEach((adjType) => {
if (adjType === 'add') {
if (objToModify[k] === undefined) {
objToModify[k] = modItem[k][adjType];
}
else {
objToModify[k].push(...modItem[k][adjType]);
}
}
else {
console.warn(
`Could not apply data modification: Unhandled adjustment type "${adjType}" for category "${modCat}", object "${modObjID}, property ${k}"`
);
}
});
}
else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
}
);
break;
case 'pets':
Object.keys(modItem)
.filter((k) => k !== 'id')
.forEach((k) => {
if (k === 'modifiers') {
this.applyModifierModifications(objToModify, modItem[k]);
}
else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
}
);
break;
case 'equipmentSlots':
Object.keys(modItem)
.filter((k) => k !== 'id')
.forEach((k) => {
if (k === 'requirements') {
this.applyAddRemoveModifications(objToModify, modItem[k], 'requirements');
}
else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
}
);
break;
case 'slayerAreas':
Object.keys(modItem)
.filter((k) => k !== 'id')
.forEach((k) => {
if (k === 'gamemodeEntryRequirements') {
this.applyGamemodeSpecificModifications(objToModify, modItem[k], 'entryRequirements');
}
else {
console.warn(
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"`
);
}
}
);
break;
default:
console.warn(
`Could not apply data modification: Unhandled category "${modCat}"`
);
}
}
}
});
}
}
}
// Normal damage type exisst outside of data packages
}
if (this.gameData.damageTypes === undefined) {
registerNonPackData() {
this.gameData.damageTypes = game.damageTypes
// Some data resides outside of packages. Add any such data to this.gameData within this function
.filter((d) => d.id === 'melvorD:Normal')
// Metadata for data/file version
.map((d) => ({
if (this.gameData._dataVersion === undefined) {
id: d.id,
this.gameData._dataVersion = ({
name: d.name,
gameVersion: this.getGameVersion().substring(1),
resistanceCap: d._resistanceCap,
fileVersion: this.getGameFileVersion().substring(1)
resistanceName: d.resistanceName
}));
}
/**
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;
}
}
*/
// Namespaces
/**
if (this.gameData.namespaces === undefined) {
if (this.gameData.masteryCheckpoints === undefined) {
const nsData = [];
this.gameData.masteryCheckpoints = masteryCheckpoints;
game.registeredNamespaces.forEach((ns) => {
}
if (ns.isModded) {
*/
throw new Error(
if (this.gameData.combatAreaDifficulties === undefined) {
`Modded namespace '${ns.displayName}' found, all mods must be disabled before game data can be generated`
this.gameData.combatAreaDifficulties = CombatAreaMenuElement.difficulty.map((i) => i.name);
);
}
} else {
/**
nsData.push(ns);
if (this.gameData.equipmentSlots === undefined) {
}
const slotIDs = Object.keys(EquipmentSlots).filter((slotID) => !isNaN(parseInt(slotID)));
});
this.gameData.equipmentSlots = slotIDs.map((slotID) => ({
this.gameData.namespaces = nsData;
id: EquipmentSlots[slotID],
name: this.getLangString('EQUIP_SLOT', slotID),
}));
}
}
*/
if (this.gameData.currencies === undefined) {
if (this.gameData.attackTypes === undefined) {
this.gameData.currencies = game.currencies.allObjects.map((c) => ({
this.gameData.attackTypes = AttackTypeID;
id: c.id,
name: c.name,
type: c.type
}));
}
}
/**
// Melvor realm exists outside of data packages
if (this.gameData.slayerTiers === undefined) {
if (this.gameData.realms === undefined) {
const newData = structuredClone(SlayerTask.data);
this.gameData.realms = game.realms
newData.forEach((tier) => delete tier.engDisplay);
.filter((r) => r.id === 'melvorD:Melvor')
this.gameData.slayerTiers = newData;
.map((r) => ({
id: r.id,
name: r.name,
unlockRequirements: r.unlockRequirements
}));
}
}
*/
// Normal damage type exists outside of data packages
/**
if (this.gameData.damageTypes === undefined) {
if (this.gameData.modifierData === undefined && modifierData !== undefined) {
this.gameData.damageTypes = game.damageTypes
var wikiModData = {};
.filter((d) => d.id === 'melvorD:Normal')
Object.keys(modifierData).forEach((modK) => {
.map((d) => ({
const mod = modifierData[modK];
id: d.id,
wikiModData[modK] = {};
name: d.name,
Object.keys(mod).forEach((k) => {
resistanceCap: d._resistanceCap,
if (k === 'modifyValue') {
resistanceName: d.resistanceName
// 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 (this.gameData.combatAreaDifficulties === undefined) {
if (funcName === 'modifyValue') {
this.gameData.combatAreaDifficulties = CombatAreaMenuElement.difficulty.map((i) => i.name);
funcName = mod[k].toString();
}
}
if (this.gameData.attackTypes === undefined) {
wikiModData[modK][k] = funcName;
this.gameData.attackTypes = AttackTypeID;
} 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) {
Line 1,046: Line 1,172:
const newStats = {};
const newStats = {};
entity.forEach((stat) => {
entity.forEach((stat) => {
if (newStats[stat.key] === undefined) {
let statKey = stat.key;
newStats[stat.key] = stat.value;
if (stat.damageType !== undefined) {
statKey += this.getLocalID(stat.damageType);
}
if (newStats[statKey] === undefined) {
newStats[statKey] = stat.value;
} else {
} else {
newStats[stat.key] += stat.value;
newStats[statKey] += stat.value;
}
}
});
});
Line 1,114: Line 1,244:
const purchase = game.shop.purchases.getObjectByID(data.id);
const purchase = game.shop.purchases.getObjectByID(data.id);
if (purchase !== undefined) {
if (purchase !== undefined) {
return purchase.description;
// Logic taken from description method of ShopPurchase class & slightly modified
// to avoid retrieving an item's modified description, which can include HTML
let desc = '';
if (purchase._customDescription !== undefined) {
if (purchase.isModded) {
return purchase._customDescription;
}
else {
return getLangString(`SHOP_DESCRIPTION_${ purchase.localID }`);
}
}
if (purchase.contains.itemCharges !== undefined) {
  return purchase.contains.itemCharges.item.description;
}
if (purchase.contains.items.length === 1) {
  return purchase.contains.items[0].item.description; // Was modifiedDescription
}
if (purchase.contains.pet !== undefined) {
  return purchase.contains.pet.description;
}
if (purchase.contains.stats !== undefined) {
  desc = purchase.contains.stats.describePlain();
}
if (purchase.hasDisabledModifier) {
desc += getLangString('MENU_TEXT_CONTAINS_DISABLED_MODIFIER');
}
return desc;
} else return '';
} else return '';
};
};
Line 1,156: Line 1,312:
const spell = game.attackSpells.getObjectByID(data.id);
const spell = game.attackSpells.getObjectByID(data.id);
if (spell !== undefined) {
if (spell !== undefined) {
return spell.name;
return this.getLangString(`${ spell.spellbook.spellNameLangPrefix }${ spell.localID }`);
}
}
};
};
Line 1,165: Line 1,321:
'combatAreaDisplayOrder',
'combatAreaDisplayOrder',
'combatAreaCategoryOrder',
'combatAreaCategoryOrder',
'combatEffectTables',
'combatEffectTemplates',
'combatEffectTemplates',
'combatEvents',
'combatEvents',
Line 1,217: Line 1,374:
name: { key: 'MAGIC', idFormat: 'AURORA_NAME_{ID}' },
name: { key: 'MAGIC', idFormat: 'AURORA_NAME_{ID}' },
},
},
combatAreaCategories: {
combatAreaCategories: {
name: { key: 'COMBAT_AREA_CATEGORY' }
name: { key: 'COMBAT_AREA_CATEGORY' }
},
},
combatAreas: {
combatAreas: {
name: { key: 'COMBAT_AREA', idFormat: 'NAME_{ID}' },
name: { key: 'COMBAT_AREA', idFormat: 'NAME_{ID}' },
},
},
combatEffectGroups: {
combatEffectGroups: {
name: { idKey: 'nameLang' }
name: { idKey: 'nameLang' }
},
},
combatEffects: {
combatEffects: {
name: { idKey: 'nameLang' }
name: { idKey: 'nameLang' }
},
},
combatPassives: {
combatPassives: {
name: { key: 'PASSIVES', idFormat: 'NAME_{ID}' },
name: { key: 'PASSIVES', idFormat: 'NAME_{ID}' },
customDescription: { stringSpecial: 'passiveDesc' },
customDescription: { stringSpecial: 'passiveDesc' },
//customDescription: { key: 'PASSIVES', idFormat: 'DESC_{ID}' }
//customDescription: { key: 'PASSIVES', idFormat: 'DESC_{ID}' }
},
curseSpells: {
name: { key: 'MAGIC', idFormat: 'CURSE_NAME_{ID}' },
},
dungeons: {
name: { key: 'DUNGEON', idFormat: 'NAME_{ID}' },
},
abyssDepths: {
name: { key: 'THE_ABYSS', idFormat: 'NAME_{ID}' },
},
strongholds: {
name: { idFormat: 'STRONGHOLD_NAME_{ID}' },
},
},
curseSpells: {
equipmentSlots: {
name: { key: 'MAGIC', idFormat: 'CURSE_NAME_{ID}' },
emptyName: { idFormat: 'EQUIP_SLOT_{ID}' }
},
dungeons: {
name: { key: 'DUNGEON', idFormat: 'NAME_{ID}' },
},
abyssDepths: {
name: { key: 'THE_ABYSS', idFormat: 'NAME_{ID}' },
},
strongholds: {
name: { key: 'STRONGHOLD_NAME', idFormat: 'NAME_{ID}' },
},
},
gamemodes: {
gamemodes: {
Line 1,667: Line 1,827:
lookupVal += (lookupVal.length > 0 ? '_' : '') + identifier;
lookupVal += (lookupVal.length > 0 ? '_' : '') + identifier;
}
}
return loadedLangJson[lookupVal];
return this.customLocalizations[lookupVal] ?? loadedLangJson[lookupVal];
}
}
getNamespacedID(namespace, ID) {
getNamespacedID(namespace, ID) {