17,376
edits
(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 + " | melvorD: { displayName: "Demo", url: "https://" + location.hostname + this.baseDir + "melvorDemo.json" }, | ||
melvorF: { displayName: "Full Version", url: "https://" + location.hostname + " | melvorF: { displayName: "Full Version", url: "https://" + location.hostname + this.baseDir + "melvorFull.json" }, | ||
melvorTotH: { displayName: "Throne of the Herald", url: "https://" + location.hostname + "/ | 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 ( | 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) => { | ||
const inclCat = this.getCategoriesForPage(page); | |||
let gameDataFiltered = {}; | let gameDataFiltered = {}; | ||
inclCat.forEach((cat) => gameDataFiltered[cat] = | inclCat.forEach((cat) => gameDataFiltered[cat] = this.gameData[cat]); | ||
// 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) => | 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 = | 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( | 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']; | ||
if (handlerFunc !== undefined) { | |||
switch(handlerFunc) { | |||
case 'mapPortals': | |||
Object.keys(target).forEach((portalKey) => { | |||
let portalData = target[portalKey]; | |||
const langID = this.getLocalID(portalData.originWorldMap) + '_' + this.getLocalID(portalData.id); | |||
portalData.name = this.getLangString('POI_NAME', langID); | |||
portalData.description = this.getLangString('POI_DESCRIPTION', langID); | |||
}); | |||
break; | |||
case 'cartoMaps': | |||
// Target represents a world map | |||
const mapID = this.getLocalID(target.id); | |||
target.name = this.getLangString('WORLD_MAP_NAME', mapID); | |||
// Process POIs | |||
target.pointsOfInterest.forEach((poi) => { | |||
const langID = mapID + '_' + this.getLocalID(poi.id); | |||
poi.name = this.getLangString('POI_NAME', langID); | |||
poi.description = this.getLangString('POI_DESCRIPTION', langID); | |||
}); | |||
break; | |||
} | |||
} | |||
else { | |||
Object.keys(langKeyData[langKey]).forEach((langPropID) => { | |||
const langProp = langKeyData[langKey][langPropID]; | |||
if (!langProp.onlyIfExists || target[langPropID] !== undefined) { | |||
const langIDKey = langProp.idKey ?? 'id'; | |||
var langIDValue; | |||
if (Array.isArray(target[langIDKey])) { | |||
// The ID key can sometimes be an array of IDs (e.g. Summoning synergies) | |||
langIDValue = target[langIDKey].map((id) => this.getLocalID((id ?? '').toString())); | |||
} | |||
else { | |||
langIDValue = this.getLocalID((target[langIDKey] ?? '').toString()); | |||
} | } | ||
let langIdent = langProp.idFormat; | |||
if (langProp.idSpecial !== undefined) { | |||
// Use a special method to determine the ID format | |||
switch(langProp.idSpecial) { | |||
case 'altMagicDesc': | |||
langIdent = altMagicDescIDKey(target); | |||
break; | |||
case 'shopChainID': | |||
langIdent = this.getLocalID(shopChainPropKey(target, langPropID, 'id')); | |||
break; | |||
} | |||
} | } | ||
if ( | if (langIdent === undefined) { | ||
langIDValue | langIdent = langIDValue; | ||
} | } | ||
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]); | |||
}); | |||
} | } | ||
let langCategoryKey = langProp.key; | |||
if (langProp.keySpecial !== undefined) { | |||
// Use a special method to determine the category key | |||
switch(langProp.keySpecial) { | |||
case 'shopChainKey': | |||
langCategoryKey = shopChainPropKey(target, langPropID, 'category'); | |||
break; | |||
} | |||
} | } | ||
if (Array.isArray(target[langPropID])) { | |||
target[langPropID].forEach((targetElem, num) => { | |||
const langIdentFinal = langIdent.replaceAll('{NUM}', num.toString()); | |||
const langString = this.getLangString(langCategoryKey, langIdentFinal); | |||
target[langPropID][num] = langString; | |||
if (this.debugMode) { | |||
if (langString !== undefined) { | |||
console.debug('Set value of property ' + langPropID + '[' + num.toString() + '] for ' + langIdentFinal + ' in node ' + nodeName + ' to: ' + langString); | |||
} | |||
else { | |||
console.debug('No translation: property ' + langPropID + ' for ' + langIdentFinal + ' in node ' + nodeName); | |||
} | |||
} | |||
}); | |||
} | |||
else { | |||
let langString; | |||
if (langProp.stringSpecial !== undefined) { | |||
// Use a custom function to determine the string | |||
switch(langProp.stringSpecial) { | |||
case 'itemDesc': | |||
langString = itemDesc(target); | |||
break; | |||
case 'passiveDesc': | |||
langString = passiveDesc(target); | |||
break; | |||
case 'relicDesc': | |||
langString = relicDesc(target); | |||
break; | |||
case 'spAttDesc': | |||
langString = spAttDesc(target); | |||
break; | |||
case 'tsWorshipName': | |||
langString = tsWorshipName(target); | |||
break; | |||
case 'tsWorshipStatueName': | |||
langString = tsWorshipStatueName(target); | |||
break; | |||
} | |||
} | |||
else { | |||
langString = this.getLangString(langCategoryKey, langIdent); | |||
} | |||
target[langPropID] = langString; | |||
if (this.debugMode) { | if (this.debugMode) { | ||
if (langString !== undefined) { | if (langString !== undefined) { | ||
console.debug('Set value of property ' + langPropID + ' | console.debug('Set value of property ' + langPropID + ' for ' + langIdent + ' in node ' + nodeName + ' to: ' + langString); | ||
} | } | ||
else { | else { | ||
console.debug('No translation: property ' + langPropID + ' for ' + | 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) { |