17,481
edits
(Amend equipmentStat handling for resistances) |
(Support all remaining data modifications & dependent data) |
||
(2 intermediate revisions by the same user not shown) | |||
Line 19: | Line 19: | ||
// To be used sparingly, for instances where 2+ objects of the same type | // 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 | // (e.g. monsters) have the same name, as this isn't convenient to deal with in Lua | ||
MAGIC_ABYSSAL_NAME_Madness: 'Madness (ItA)' // | |||
// 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 = { | ||
Line 47: | Line 57: | ||
}, | }, | ||
}; | }; | ||
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 122: | 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 255: | Line 277: | ||
const packData = dataPackage.data; | const packData = dataPackage.data; | ||
Object.keys(packData). | Object.keys(packData) | ||
.filter((categoryName) => !this.excludedCategories.includes(categoryName)) | |||
.forEach((categoryName) => { | |||
this.transformDataNode(ns, categoryName, packData, categoryName); | |||
}); | |||
} | } | ||
transformDataNode(ns, categoryName, parentNode, nodeKey) { | transformDataNode(ns, categoryName, parentNode, nodeKey) { | ||
Line 408: | Line 420: | ||
} | } | ||
} | } | ||
registerPackData(packData) { | |||
Object.keys(packData) | |||
.filter((categoryName) => !this.excludedCategories.includes(categoryName)) | |||
.forEach((categoryName) => { | |||
let categoryData = packData[categoryName]; | let categoryData = packData[categoryName]; | ||
// Some data is adjusted before combining - do this here | // Some data is adjusted before combining - do this here | ||
Line 434: | Line 439: | ||
newData.forEach((x) => (x.type = areaType)); | newData.forEach((x) => (x.type = areaType)); | ||
categoryData = newData; | 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 600: | 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 605: | Line 614: | ||
this.applyDataModifications(modificationData); | this.applyDataModifications(modificationData); | ||
} | } | ||
// Dependent data is handled later, once all packages have been registered | |||
if ( | |||
if (!this.registeredNamespaces.includes(namespace)) { | |||
this.registeredNamespaces.push(namespace); | |||
} | } | ||
} | |||
processDependentData() { | |||
Object.entries(this.packData) | |||
.forEach(([namespace, packData]) => { | |||
if (packData.dependentData !== undefined) { | |||
packData.dependentData.forEach((depDataForNS) => { | |||
const depNS = depDataForNS.namespace; | |||
if (!this.registeredNamespaces.includes(depNS)) { | |||
console.warn( | |||
`Could not apply dependent data from package ${namespace}: Data depends on namespace ${depNS}, which has not been registered` | |||
); | |||
} | |||
else { | |||
console.log(`Attempting to apply dependent data for ${depNS} from package ${namespace}`); | |||
if (depDataForNS.data !== undefined) { | |||
this.registerPackData(depDataForNS.data) | |||
} | |||
if (depDataForNS.modifications !== undefined) { | |||
this.applyDataModifications(depDataForNS.modifications); | |||
} | |||
} | |||
}); | |||
} | |||
}); | |||
} | } | ||
getDataToModify(modCat) { | getDataToModify(modCat) { | ||
switch (modCat) { | switch (modCat) { | ||
case 'combatAreaCategories': | case 'combatAreaCategories': | ||
case 'damageTypes': | |||
case 'dungeons': | case 'dungeons': | ||
case 'equipmentSlots': | case 'equipmentSlots': | ||
case 'gamemodes': | |||
case 'items': | |||
case 'modifiers': | case 'modifiers': | ||
case 'pets': | |||
case 'shopUpgradeChains': | case 'shopUpgradeChains': | ||
case 'shopPurchases': | case 'shopPurchases': | ||
case 'skillData': | |||
case 'slayerAreas': | |||
return this.gameData[modCat]; | return this.gameData[modCat]; | ||
case 'cookingCategories': | case 'cookingCategories': | ||
Line 630: | Line 667: | ||
} | } | ||
return undefined; | return undefined; | ||
} | |||
applyModifierModifications(objToModify, adjustments) { | |||
if (objToModify.modifiers === undefined) { | |||
objToModify.modifiers = {}; | |||
} | |||
Object.keys(adjustments) | |||
.forEach((adjType) => { | |||
if (adjType === 'add') { | |||
Object.entries(adjustments[adjType]) | |||
.forEach(([chgKey, chgVal]) => { | |||
if (objToModify.modifiers[chgKey] === undefined) { | |||
objToModify.modifiers[chgKey] = chgVal; | |||
} | |||
else if (Array.isArray(chgVal)) { | |||
objToModify.modifiers[chgKey].push(...chgVal); | |||
} | |||
else { | |||
objToModify.modifiers[chgKey] += chgVal; | |||
} | |||
}); | |||
} | |||
else { | |||
console.warn( | |||
`Could not apply data modification: Unhandled modifier adjustment "${adjType}"` | |||
); | |||
} | |||
} | |||
); | |||
} | |||
applyAddRemoveModifications(objToModify, adjustments, modifyKey) { | |||
if (adjustments.remove !== undefined && Array.isArray(objToModify[modifyKey])) { | |||
// adjustments.remove is an array of requirement types to be removed | |||
let i = 0; | |||
while (i < objToModify[modifyKey].length) { | |||
if (adjustments.remove.includes(objToModify[modifyKey][i].type)) { | |||
objToModify[modifyKey].splice(i, 1); | |||
} | |||
else { | |||
i++; | |||
} | |||
} | |||
} | |||
if (adjustments.add !== undefined) { | |||
if (objToModify[modifyKey] === undefined) { | |||
objToModify[modifyKey] = adjustments.add; | |||
} | |||
else { | |||
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) { | applyDataModifications(modData) { | ||
const modDataKeys = Object.keys(modData).filter((modCatID) => !this.excludedCategories.includes(modCatID)); | |||
const modDataKeys = Object.keys(modData); | |||
for (const modCatID in modDataKeys) { | for (const modCatID in modDataKeys) { | ||
const modCat = modDataKeys[modCatID]; | const modCat = modDataKeys[modCatID]; | ||
const catData = modData[modCat]; | const catData = modData[modCat]; | ||
const dataToModify = this.getDataToModify(modCat); | const dataToModify = this.getDataToModify(modCat); | ||
const modObjIDKey = (modCat === 'skillData' ? 'skillID' : 'id'); | |||
if (dataToModify === undefined) { | if (dataToModify === undefined) { | ||
console.warn( | console.warn( | ||
Line 649: | Line 750: | ||
else { | else { | ||
catData.forEach((modItem) => { | catData.forEach((modItem) => { | ||
const modObjID = modItem | const modObjID = modItem[modObjIDKey]; | ||
if (modObjID === undefined) { | if (modObjID === undefined) { | ||
console.warn( | console.warn( | ||
Line 655: | Line 756: | ||
); | ); | ||
} else { | } else { | ||
const objToModify = this.getObjectByID(dataToModify, modObjID); | const objToModify = this.getObjectByID(dataToModify, modObjID, modObjIDKey); | ||
if (objToModify === undefined) { | if (objToModify === undefined) { | ||
console.warn( | console.warn( | ||
Line 666: | Line 767: | ||
// The 'areas' property of elements within the category data are ordered data | // The 'areas' property of elements within the category data are ordered data | ||
objToModify.areas = this.combineOrderedData(objToModify.areas, modItem.areas.add); | objToModify.areas = this.combineOrderedData(objToModify.areas, modItem.areas.add); | ||
break; | |||
case 'damageTypes': | |||
Object.entries(modItem) | |||
.filter(([k, v]) => k !== 'id') | |||
.forEach(([k, v]) => { | |||
if (typeof v === 'object' && (v.add !== undefined || v.remove !== undefined)) { | |||
this.applyAddRemoveModifications(objToModify, v, k); | |||
} | |||
else { | |||
console.warn( | |||
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"` | |||
); | |||
} | |||
}); | |||
break; | |||
case 'gamemodes': | |||
Object.entries(modItem) | |||
.filter(([k, v]) => k !== 'id') | |||
.forEach(([k, v]) => { | |||
if (typeof v === 'object' && (v.add !== undefined || v.remove !== undefined)) { | |||
this.applyAddRemoveModifications(objToModify, v, k); | |||
} | |||
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}"` | |||
); | |||
} | |||
}); | |||
break; | break; | ||
case 'shopPurchases': | case 'shopPurchases': | ||
Line 719: | Line 851: | ||
} else { | } else { | ||
console.warn( | console.warn( | ||
`Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${ | `Could not apply data modification: Unhandled key "${k}" for category "${modCat}", object "${modObjID}"` | ||
); | ); | ||
} | } | ||
}); | |||
break; | |||
case 'skillData': | |||
Object.entries(modItem.data) | |||
.forEach(([skillProp, propModData]) => { | |||
propModData.forEach((subModItem) => { | |||
const subObjToModify = this.getObjectByID(objToModify.data[skillProp], subModItem.id); | |||
if (subObjToModify === undefined) { | |||
console.warn(`Couldn't find skill object with ID ${subModItem.id} to modify. Property ${skillProp} in skill ID ${objToModify.skillID}`); | |||
} | |||
else { | |||
Object.entries(subModItem) | |||
.forEach(([subProp, subData]) => { | |||
if (subProp === 'modifiers') { | |||
this.applyModifierModifications(subObjToModify, subData); | |||
} | |||
else if (subProp !== 'id') { | |||
this.applyAddRemoveModifications(subObjToModify, subData, subProp); | |||
} | |||
}); | |||
} | |||
}); | |||
}); | }); | ||
break; | break; | ||
Line 734: | Line 888: | ||
Object.keys(itemRules).forEach((ruleKey) => { | Object.keys(itemRules).forEach((ruleKey) => { | ||
if (ruleKey === 'add') { | if (ruleKey === 'add') { | ||
itemRules[ruleKey].forEach((itemDef) => { | itemRules[ruleKey].forEach((itemDef) => { | ||
const modToApply = { | |||
gamemodeID: itemDef.gamemodeID, | |||
add: itemDef.rewardItemIDs | |||
} | } | ||
this.applyGamemodeSpecificModifications(objToModify, modToApply, 'rewardItemIDs'); | |||
}); | }); | ||
} else { | } else { | ||
Line 756: | Line 903: | ||
} else if (k === 'gamemodeEntryRequirements') { | } else if (k === 'gamemodeEntryRequirements') { | ||
// Add or remove gamemode specific entry requirements to dungeon data | // Add or remove gamemode specific entry requirements to dungeon data | ||
this.applyGamemodeSpecificModifications(objToModify, modItem[k], 'entryRequirements'); | |||
} else { | } else { | ||
console.warn( | console.warn( | ||
Line 797: | Line 941: | ||
} | } | ||
break; | 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: | default: | ||
console.warn( | console.warn( | ||
Line 850: | Line 1,070: | ||
})); | })); | ||
} | } | ||
// Normal damage type | // Normal damage type exists outside of data packages | ||
if (this.gameData.damageTypes === undefined) { | if (this.gameData.damageTypes === undefined) { | ||
this.gameData.damageTypes = game.damageTypes | this.gameData.damageTypes = game.damageTypes | ||
Line 861: | Line 1,081: | ||
})); | })); | ||
} | } | ||
if (this.gameData.combatAreaDifficulties === undefined) { | if (this.gameData.combatAreaDifficulties === undefined) { | ||
this.gameData.combatAreaDifficulties = CombatAreaMenuElement.difficulty.map((i) => i.name); | this.gameData.combatAreaDifficulties = CombatAreaMenuElement.difficulty.map((i) => i.name); | ||
} | } | ||
if (this.gameData.attackTypes === undefined) { | if (this.gameData.attackTypes === undefined) { | ||
this.gameData.attackTypes = AttackTypeID; | this.gameData.attackTypes = AttackTypeID; | ||
} | } | ||
} | } | ||
combineOrderedData(existingData, newData) { | combineOrderedData(existingData, newData) { | ||
Line 1,158: | Line 1,321: | ||
'combatAreaDisplayOrder', | 'combatAreaDisplayOrder', | ||
'combatAreaCategoryOrder', | 'combatAreaCategoryOrder', | ||
'combatEffectTables', | |||
'combatEffectTemplates', | 'combatEffectTemplates', | ||
'combatEvents', | 'combatEvents', |