Module:GameData/doc: Difference between revisions

From Melvor Idle
(Support shop purchases for data modifications, and suppress language data warnings)
(Update for v1.2/AoD)
Line 11: Line 11:
this.debugMode = false;
this.debugMode = false;
this.prettyPrint = false;
this.prettyPrint = false;
this.baseDir = "/assets/data/";
this.namespaces = {
this.namespaces = {
melvorD: { displayName: "Demo", url: "https://" + location.hostname + "/assets/data/melvorDemo.json" },
melvorD: { displayName: "Demo", url: "https://" + location.hostname + this.baseDir + "melvorDemo.json" },
melvorF: { displayName: "Full Version", url: "https://" + location.hostname + "/assets/data/melvorFull.json" },
melvorF: { displayName: "Full Version", url: "https://" + location.hostname + this.baseDir + "melvorFull.json" },
melvorTotH: { displayName: "Throne of the Herald", url: "https://" + location.hostname + "/assets/data/melvorTotH.json" }
melvorTotH: { displayName: "Throne of the Herald", url: "https://" + location.hostname + this.baseDir + "melvorTotH.json" },
melvorAoD: { displayName: "Atlas of Discovery", url: "https://" + location.hostname + this.baseDir + "melvorExpansion2.json" }
};
};
// Check all required namespaces are registered, as there are still some bits of data extracted from in-game rather than the data packages
// Check all required namespaces are registered, as there are still some bits of data extracted from in-game rather than the data packages
Line 27: Line 29:
// pages (Module:GameData then combines the data into a single structure upon
// pages (Module:GameData then combines the data into a single structure upon
// initialization).
// initialization).
this.maxPageBytes = 2*1024**2; // 2048KB
this.printPages = [
this.printPages = [
{ includeCategories: '*', destination: 'Module:GameData/data' },
{ includeCategories: '*', destination: 'Module:GameData/data' },
{ includeCategories: ['items'], destination: 'Module:GameData/data2' }
{ includeCategories: ['items', 'itemUpgrades', 'itemSynergies', 'modifierData', 'shopPurchases'], destination: 'Module:GameData/data2' }
];
];


Line 69: Line 72:
return data.find((obj) => obj[idKey] === objectID);
return data.find((obj) => obj[idKey] === objectID);
}
}
}
getCategoriesForPage(page) {
if (Array.isArray(page.includeCategories)) {
return page.includeCategories;
}
else if (page.includeCategories === '*') {
// Special value, include all categories other than those included within
// other pages
return Object.keys(this.gameData).filter((cat) => !this.printPages.some((p) => Array.isArray(p.includeCategories) && p.includeCategories.includes(cat)));
}
}
escapeQuotes(data) {
var newData = data.replace(/\'/g, "\\\'");
newData = newData.replace(/\\\"/g, "\\\\\"");
return newData;
}
formatJSONData(category, data) {
if (data === undefined) {
console.warn(`dataFormatter: Data for category ${ category } is undefined`);
return '';
}
if (this.debugMode) {
console.debug('Formatting category data: ' + category);
}
if (category === 'skillData') {
return '"' + category + '":[' + data.map((x) => this.escapeQuotes(JSON.stringify(x))).join(",' ..\n'") + ']';
}
else {
return '"' + category + '":' + this.escapeQuotes(JSON.stringify(data));
}
}
dataFullyLoaded() {
return Object.keys(this.packData).length >= Object.keys(this.namespaces).length;
}
printCategoryDataLength() {
if (!this.dataFullyLoaded()) {
throw new Error('Game data not loaded, use printWikiData first');
}
let dataLengths = [];
this.printPages.forEach((page) => {
const inclCat = this.getCategoriesForPage(page);
inclCat.forEach((cat) => {
dataLengths.push(({
page: page.destination,
category: cat,
length: this.formatJSONData(cat, this.gameData[cat]).length
}));
});
});
console.table(dataLengths);
}
}
async printWikiData() {
async printWikiData() {
Line 74: Line 127:
throw new Error('Game must be loaded into a character first');
throw new Error('Game must be loaded into a character first');
}
}
if (Object.keys(this.packData).length < Object.keys(this.namespaces).length) {
if (!this.dataFullyLoaded()) {
// Need to retrieve game data first
// Need to retrieve game data first
const result = await this.getWikiData();
const result = await this.getWikiData();
Line 80: Line 133:
let dataObjText;
let dataObjText;
this.printPages.forEach((page) => {
this.printPages.forEach((page) => {
let inclCat = [];
const inclCat = this.getCategoriesForPage(page);
if (Array.isArray(page.includeCategories)) {
inclCat = page.includeCategories;
}
else if (page.includeCategories === '*') {
// Special value, include all categories other than those included within
// other pages
inclCat = Object.keys(this.gameData).filter((cat) => !this.printPages.some((p) => Array.isArray(p.includeCategories) && p.includeCategories.includes(cat)));
}
let gameDataFiltered = {};
let gameDataFiltered = {};
inclCat.forEach((cat) => gameDataFiltered[cat] = wd.gameData[cat]);
inclCat.forEach((cat) => gameDataFiltered[cat] = this.gameData[cat]);


const escapeQuotes = (data) => {
var newData = data.replace(/\'/g, "\\\'");
newData = newData.replace(/\\\"/g, "\\\\\"");
return newData;
};
const dataFormatter = (category, data) => {
if (category === 'skillData') {
return '"' + category + '":[' + data.map((x) => escapeQuotes(JSON.stringify(x))).join(",' ..\n'") + ']';
}
else {
return '"' + category + '":' + escapeQuotes(JSON.stringify(data));
}
};
// Convert game data into a JSON string for export
// Convert game data into a JSON string for export
dataObjText = undefined;
dataObjText = undefined;
if (this.prettyPrint) {
if (this.prettyPrint) {
dataObjText = escapeQuotes(JSON.stringify(gameDataFiltered, undefined, '\t'));
dataObjText = this.escapeQuotes(JSON.stringify(gameDataFiltered, undefined, '\t'));
}
}
else {
else {
dataObjText = "{" + Object.keys(gameDataFiltered).map((k) => dataFormatter(k, gameDataFiltered[k])).join(",' ..\n'") + "}"; //JSON.stringify(gameDataFiltered);
dataObjText = "{" + Object.keys(gameDataFiltered).map((k) => this.formatJSONData(k, gameDataFiltered[k])).join(",' ..\n'") + "}"; //JSON.stringify(gameDataFiltered);
}
}
Line 119: Line 151:
dataText += "')\r\n\r\nreturn gameData";
dataText += "')\r\n\r\nreturn gameData";
console.log(`For page "${ page.destination }" (${ dataText.length.toLocaleString() } bytes):`);
console.log(`For page "${ page.destination }" (${ dataText.length.toLocaleString() } bytes):`);
if (dataText.length > this.maxPageBytes) {
console.warn(`Page "${ page.destination }" exceeds max page size of ${ (this.maxPageBytes / 1024).toLocaleString() }KB by ${ (dataText.length - this.maxPageBytes).toLocaleString() } bytes. Consider amending the printPages configuration to move some data categories from this page onto other pages.`)
}
console.log(dataText);
console.log(dataText);
  });
  });
Line 357: Line 392:
// depending on the category in question
// depending on the category in question
switch(categoryName) {
switch(categoryName) {
case 'ancientRelics':
case 'ancientSpells':
case 'ancientSpells':
case 'archaicSpells':
case 'archaicSpells':
Line 484: Line 520:
if (modificationData !== undefined) {
if (modificationData !== undefined) {
this.applyDataModifications(modificationData);
this.applyDataModifications(modificationData);
}
const dependentData = this.packData[namespace].dependentData;
if (dependentData !== undefined) {
// TODO Handle dependentData
}
}
}
}
Line 504: Line 544:
}
}
else {
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) => {
Object.keys(modItem).filter((k) => k !== 'id').forEach((k) => {
modObj[k] = modItem[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];
}
});
});
}
}
Line 573: Line 642:
else {
else {
console.warn(`Could not apply data modification: Unhandled key "${ k }" for category "${ modCat }", object "${ mobObjID }"`);
console.warn(`Could not apply data modification: Unhandled key "${ k }" for category "${ modCat }", object "${ mobObjID }"`);
}
});
}
}
});
}
else if (modCat === 'dungeons') {
catData.forEach((modItem) => {
const modObjID = modItem.id;
if (modObjID === undefined) {
console.warn(`Could not apply data modification: ID of object to be modified not found, category "${ modCat }"`);
}
else {
const modObj = this.getObjectByID(this.gameData.dungeons, modObjID);
if (modObj === undefined) {
console.warn(`Could not apply data modification: Object with ID "${ modObjID }" not found for category "${ modCat }"`);
}
else {
Object.keys(modItem).filter((k) => k !== 'id').forEach((k) => {
if (k === 'gamemodeRewardItemIDs') {
// Add gamemode specific item rewards to dungeon data
const itemRules = modItem[k];
Object.keys(itemRules).forEach((ruleKey) => {
if (ruleKey === 'add') {
if (modObj[k] === undefined) {
modObj[k] = [];
}
itemRules[ruleKey].forEach((itemDef) => {
let gamemodeRewards = this.getObjectByID(modObj[k], itemDef.gamemodeID, 'gamemodeID');
if (gamemodeRewards === undefined) {
modObj[k].push(({
gamemodeID: itemDef.gamemodeID,
itemIDs: itemDef.rewardItemIDs
}));
}
else {
gamemodeRewards.push(...itemDef.rewardItemIDs);
}
});
}
else {
console.warn(`Could not apply data modification: Unknown rule for gamemode item rewards: "${ ruleKey }", object "${ modObjID }"`);
}
});
}
else if (k === 'gamemodeEntryRequirements') {
// Add or remove gamemode specific entry requirements to dungeon data
if (modObj[k] === undefined) {
modObj[k] = [];
}
modObj[k].push(modItem[k]);
}
else {
console.warn(`Could not apply data modification: Unhandled key "${ k }" for category "${ modCat }", object "${ modObjID }"`);
}
}
});
});
Line 611: Line 734:
}
}
if (this.gameData.combatAreaDifficulties === undefined) {
if (this.gameData.combatAreaDifficulties === undefined) {
this.gameData.combatAreaDifficulties = CombatAreaMenu.difficulty.map((i) => i.name);
this.gameData.combatAreaDifficulties = CombatAreaMenuElement.difficulty.map((i) => i.name);
}
}
if (this.gameData.equipmentSlots === undefined) {
if (this.gameData.equipmentSlots === undefined) {
Line 674: Line 797:
throw new Error(`Couldn't insert before: Item ${ orderData.beforeID } is not in the array.`);
throw new Error(`Couldn't insert before: Item ${ orderData.beforeID } is not in the array.`);
}
}
resultData.splice(beforeIndex, 0, ...orderData.ids);
resultData.splice(beforeIdx, 0, ...orderData.ids);
break;
break;
case 'After':
case 'After':
Line 817: Line 940:
return desc;
return desc;
}
}
}
}
const relicDesc = (data) => {
const relic = game.ancientRelics.getObjectByID(data.id);
if (relic !== undefined) {
return relic.name;
}
}
}
}
Line 863: Line 992:
];
];
const langKeys = {
const langKeys = {
ancientRelics: {
name: { stringSpecial: 'relicDesc' }
},
ancientSpells: {
ancientSpells: {
name: { key: 'MAGIC', idFormat: 'ANCIENT_NAME_{ID}' }
name: { key: 'MAGIC', idFormat: 'ANCIENT_NAME_{ID}' }
Line 954: Line 1,086:
description: { key: 'MASTERY_BONUS', idKey: 'descriptionID', idFormat: '{SKILLID}_{ID}' }
description: { key: 'MASTERY_BONUS', idKey: 'descriptionID', idFormat: '{SKILLID}_{ID}' }
}
}
},
Archaeology: {
digSites: {
name: { key: 'POI_NAME_Melvor' }
}
// TODO Tool names
},
},
Agility: {
Agility: {
Line 970: Line 1,108:
name: { key: 'ASTROLOGY', idFormat: 'NAME_{ID}' }
name: { key: 'ASTROLOGY', idFormat: 'NAME_{ID}' }
}
}
},
Cartography: {
mapPortals: { _handler: 'mapPortals' },
travelEvents: {
description: { key: 'TRAVEL_EVENT' }
},
worldMaps: { _handler: 'cartoMaps' }
//name: { key: 'WORLD_MAP_NAME' },
//pointsOfInterest: { _handler: 'mapPOI' }
//name: { key: 'POI_NAME', idFormat: '{MAPID}_{ID}' },
//description: { key: 'POI_DESCRIPTION', idFormat: '{MAPID}_{ID}' }
},
},
Farming: {
Farming: {
Line 1,089: Line 1,238:
const targetArr = (Array.isArray(targetData) ? targetData : [ targetData ]);
const targetArr = (Array.isArray(targetData) ? targetData : [ targetData ]);
targetArr.forEach((target) => {
targetArr.forEach((target) => {
Object.keys(langKeyData[langKey]).forEach((langPropID) => {
const handlerFunc = langKeyData[langKey]['_handler'];
const langProp = langKeyData[langKey][langPropID];
if (handlerFunc !== undefined) {
if (!langProp.onlyIfExists || target[langPropID] !== undefined) {
switch(handlerFunc) {
const langIDKey = langProp.idKey ?? 'id';
case 'mapPortals':
var langIDValue;
Object.keys(target).forEach((portalKey) => {
if (Array.isArray(target[langIDKey])) {
let portalData = target[portalKey];
// The ID key can sometimes be an array of IDs (e.g. Summoning synergies)
const langID = this.getLocalID(portalData.originWorldMap) + '_' + this.getLocalID(portalData.id);
langIDValue = target[langIDKey].map((id) => this.getLocalID((id ?? '').toString()));
portalData.name = this.getLangString('POI_NAME', langID);
}
portalData.description = this.getLangString('POI_DESCRIPTION', langID);
else {
});
langIDValue = this.getLocalID((target[langIDKey] ?? '').toString());
break;
}
case 'cartoMaps':
let langIdent = langProp.idFormat;
// Target represents a world map
if (langProp.idSpecial !== undefined) {
const mapID = this.getLocalID(target.id);
// Use a special method to determine the ID format
target.name = this.getLangString('WORLD_MAP_NAME', mapID);
switch(langProp.idSpecial) {
// Process POIs
case 'altMagicDesc':
target.pointsOfInterest.forEach((poi) => {
langIdent = altMagicDescIDKey(target);
const langID = mapID + '_' + this.getLocalID(poi.id);
break;
poi.name = this.getLangString('POI_NAME', langID);
case 'shopChainID':
poi.description = this.getLangString('POI_DESCRIPTION', langID);
langIdent = this.getLocalID(shopChainPropKey(target, langPropID, 'id'));
});
break;
break;
}
}
else {
Object.keys(langKeyData[langKey]).forEach((langPropID) => {
const langProp = langKeyData[langKey][langPropID];
if (!langProp.onlyIfExists || target[langPropID] !== undefined) {
const langIDKey = langProp.idKey ?? 'id';
var langIDValue;
if (Array.isArray(target[langIDKey])) {
// The ID key can sometimes be an array of IDs (e.g. Summoning synergies)
langIDValue = target[langIDKey].map((id) => this.getLocalID((id ?? '').toString()));
}
else {
langIDValue = this.getLocalID((target[langIDKey] ?? '').toString());
}
}
}
let langIdent = langProp.idFormat;
if (langIdent === undefined) {
if (langProp.idSpecial !== undefined) {
langIdent = langIDValue;
// Use a special method to determine the ID format
}
switch(langProp.idSpecial) {
else {
case 'altMagicDesc':
// langIdent is in a specific format
langIdent = altMagicDescIDKey(target);
const langTemplate = {}
break;
if (isSkill) {
case 'shopChainID':
langTemplate.SKILLID = this.getLocalID(parentNode[nodeKey].skillID);
langIdent = this.getLocalID(shopChainPropKey(target, langPropID, 'id'));
break;
}
}
}
if (Array.isArray(langIDValue)) {
if (langIdent === undefined) {
langIDValue.forEach((val, idx) => {
langIdent = langIDValue;
langTemplate['ID' + idx] = this.getLocalID(val);
});
}
}
else {
else {
langTemplate.ID = langIDValue;
// 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]);
});
}
}
Object.keys(langTemplate).forEach((k) => {
langIdent = langIdent.replaceAll('{' + k + '}', langTemplate[k]);
});
}


let langCategoryKey = langProp.key;
let langCategoryKey = langProp.key;
if (langProp.keySpecial !== undefined) {
if (langProp.keySpecial !== undefined) {
// Use a special method to determine the category key
// Use a special method to determine the category key
switch(langProp.keySpecial) {
switch(langProp.keySpecial) {
case 'shopChainKey':
case 'shopChainKey':
langCategoryKey = shopChainPropKey(target, langPropID, 'category');
langCategoryKey = shopChainPropKey(target, langPropID, 'category');
break;
break;
}
}
}
}


if (Array.isArray(target[langPropID])) {
if (Array.isArray(target[langPropID])) {
target[langPropID].forEach((targetElem, num) => {
target[langPropID].forEach((targetElem, num) => {
const langIdentFinal = langIdent.replaceAll('{NUM}', num.toString());
const langIdentFinal = langIdent.replaceAll('{NUM}', num.toString());
const langString = this.getLangString(langCategoryKey, langIdentFinal);
const langString = this.getLangString(langCategoryKey, langIdentFinal);
target[langPropID][num] = langString;
target[langPropID][num] = langString;
if (this.debugMode) {
if (langString !== undefined) {
console.debug('Set value of property ' + langPropID + '[' + num.toString() + '] for ' + langIdentFinal + ' in node ' + nodeName + ' to: ' + langString);
}
else {
console.debug('No translation: property ' + langPropID + ' for ' + langIdentFinal + ' in node ' + nodeName);
}
}
});
}
else {
let langString;
if (langProp.stringSpecial !== undefined) {
// Use a custom function to determine the string
switch(langProp.stringSpecial) {
case 'itemDesc':
langString = itemDesc(target);
break;
case 'passiveDesc':
langString = passiveDesc(target);
break;
case 'relicDesc':
langString = relicDesc(target);
break;
case 'spAttDesc':
langString = spAttDesc(target);
break;
case 'tsWorshipName':
langString = tsWorshipName(target);
break;
case 'tsWorshipStatueName':
langString = tsWorshipStatueName(target);
break;
}
}
else {
langString = this.getLangString(langCategoryKey, langIdent);
}
target[langPropID] = langString;
if (this.debugMode) {
if (this.debugMode) {
if (langString !== undefined) {
if (langString !== undefined) {
console.debug('Set value of property ' + langPropID + '[' + num.toString() + '] for ' + langIdentFinal + ' in node ' + nodeName + ' to: ' + langString);
console.debug('Set value of property ' + langPropID + ' for ' + langIdent + ' in node ' + nodeName + ' to: ' + langString);
}
}
else {
else {
console.debug('No translation: property ' + langPropID + ' for ' + langIdentFinal + ' in node ' + nodeName);
console.debug('No translation: property ' + langPropID + ' for ' + langIdent + ' in node ' + nodeName);
}
}
}
});
}
else {
let langString;
if (langProp.stringSpecial !== undefined) {
// Use a custom function to determine the string
switch(langProp.stringSpecial) {
case 'itemDesc':
langString = itemDesc(target);
break;
case 'passiveDesc':
langString = passiveDesc(target);
break;
case 'spAttDesc':
langString = spAttDesc(target);
break;
case 'tsWorshipName':
langString = tsWorshipName(target);
break;
case 'tsWorshipStatueName':
langString = tsWorshipStatueName(target);
break;
}
}
else {
langString = this.getLangString(langCategoryKey, langIdent);
}
target[langPropID] = langString;
if (this.debugMode) {
if (langString !== undefined) {
console.debug('Set value of property ' + langPropID + ' for ' + langIdent + ' in node ' + nodeName + ' to: ' + langString);
}
else {
console.debug('No translation: property ' + langPropID + ' for ' + langIdent + ' in node ' + nodeName);
}
}
}
}
}
}
}
});
});
}
});
});
}
}
Line 1,204: Line 1,382:
}
}
getLangString(key, identifier) {
getLangString(key, identifier) {
return loadedLangJson[key + '_' + identifier];
if (identifier === undefined) {
return loadedLangJson[key];
}
else {
return loadedLangJson[key + '_' + identifier];
}
}
}
getNamespacedID(namespace, ID) {
getNamespacedID(namespace, ID) {