diff --git a/.vscode/settings.json b/.vscode/settings.json index 1071ee98..7037e56f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -64,6 +64,7 @@ "Mákhēs", "Metadatas", "Meyrin", + "moebius", "Monarque", "neomuna", "omolon", @@ -78,6 +79,7 @@ "scourgeofthepast", "seraphite", "Shaxx", + "sooper", "sotp", "sotw", "Spawnfx", diff --git a/data/exotic-synergies.ts b/data/exotic-synergies.ts new file mode 100644 index 00000000..538d4892 --- /dev/null +++ b/data/exotic-synergies.ts @@ -0,0 +1,196 @@ +import { getAllDefs, getDef } from '@d2api/manifest-node'; +import { DamageType } from 'bungie-api-ts/destiny2/interfaces.js'; +import { getComposedRegex } from '../src/helpers.js'; +import { ItemCategoryHashes, PlugCategoryHashes, SocketCategoryHashes } from './generated-enums.js'; + +const inventoryItems = getAllDefs('InventoryItem'); + +export const synergies = { + arc: { + super: getSuperNamesAndHashes(DamageType.Arc), + damageType: DamageType.Arc, + grenades: getRegexByPCH([PlugCategoryHashes.SharedArcGrenades]), + melees: getRegexByPCH([ + PlugCategoryHashes.TitanArcMelee, + PlugCategoryHashes.HunterArcMelee, + PlugCategoryHashes.WarlockArcMelee, + ]), + verbs: /blind(s)?\b|jolt(s)?/, + misc: /arc (bolt|ability|soul)|ionic traces/, + keywords: { + exclude: /sentinel shield/, + }, + }, + solar: { + super: getSuperNamesAndHashes(DamageType.Thermal), + damageType: DamageType.Thermal, + grenades: getRegexByPCH([PlugCategoryHashes.SharedSolarGrenades]), + melees: getRegexByPCH([ + PlugCategoryHashes.TitanSolarMelee, + PlugCategoryHashes.HunterSolarMelee, + PlugCategoryHashes.WarlockSolarMelee, + ]), + verbs: /scorch(es)?/, + misc: /sunspot|solar abilities|sol invictus|kni(v|f)e(s)?|helion/, + keywords: { + excludes: /solar final blows/, + }, + }, + void: { + super: getSuperNamesAndHashes(DamageType.Void), + damageType: DamageType.Void, + grenades: getRegexByPCH([PlugCategoryHashes.SharedVoidGrenades]), + melees: getRegexByPCH([ + PlugCategoryHashes.TitanVoidMelee, + PlugCategoryHashes.HunterVoidMelee, + PlugCategoryHashes.WarlockVoidMelee, + ]), + verbs: /suppress(es)?/, + misc: /void subclass|smoke bomb|void-damage|devour|invisible|blink|void soul(s)?/, + keywords: {}, + }, + stasis: { + super: getSuperNamesAndHashes(DamageType.Stasis), + damageType: DamageType.Stasis, + grenades: getRegexByPCH([PlugCategoryHashes.SharedStasisGrenades]), + melees: getRegexByPCH([ + PlugCategoryHashes.TitanStasisMelee, + PlugCategoryHashes.HunterStasisMelee, + PlugCategoryHashes.WarlockStasisMelee, + ]), + verbs: /slows/, + misc: /stasis subclass|frost armor/, + keywords: {}, + }, + strand: { + super: getSuperNamesAndHashes(DamageType.Strand), + damageType: DamageType.Strand, + grenades: getRegexByPCH([PlugCategoryHashes.SharedStrandGrenades]), + melees: getRegexByPCH([ + PlugCategoryHashes.TitanStrandMelee, + PlugCategoryHashes.HunterStrandMelee, + PlugCategoryHashes.WarlockStrandMelee, + ]), + verbs: /sever(s)?\b|!(while you're midair )suspend(s)?|unravel(s)?/, + misc: /strand subclass|woven mail/, + keywords: {}, + }, +} as Record< + string, + { + super: { name: string; hash: number; regex: RegExp }[]; + superRegex?: RegExp; + damageType: number; + grenades: RegExp; + melees: RegExp; + verbs: RegExp; + misc: RegExp; + keywords: { + include?: RegExp; + exclude?: RegExp; + }; + } +>; + +export const burns = ['arc', 'solar', 'void', 'stasis', 'strand']; + +for (const burn of burns) { + synergies[burn].superRegex = getSuperRegex(synergies[burn].super); + synergies[burn].keywords.include = getBurnKeywords(burn); +} + +function getSuperNamesAndHashes(damageType: number) { + const supers = inventoryItems.filter( + (item) => + item.itemCategoryHashes?.includes(ItemCategoryHashes.Subclasses) && + item.sockets?.socketCategories.some( + (sockets) => sockets.socketCategoryHash === SocketCategoryHashes.Super, + ) && + item.talentGrid?.hudDamageType === damageType, + ); + + const superInfo: { name: string; hash: number; regex: RegExp }[] = []; + + supers.find((item) => { + const socket = item.sockets?.socketCategories.find( + (sockets) => sockets.socketCategoryHash === SocketCategoryHashes.Super, + ); + const plugSet = getDef( + 'PlugSet', + item.sockets?.socketEntries[socket!.socketIndexes[0]].reusablePlugSetHash, + ); + let name = ''; + if (plugSet) { + for (const plug of plugSet.reusablePlugItems) { + name = + getDef('InventoryItem', plug.plugItemHash) + ?.displayProperties.name.toLowerCase() + .replace(/ - /g, '|') + .replace(/: /g, '|') ?? ''; + + // Add extra names to name if needed eg dawnblade + if (name === 'daybreak' || name === 'well of radiance') { + // Tome of Dawn references dawnblade + name += '|dawnblade'; + } + if (name === 'arc staff') { + name += '|whirlwind guard'; + } + superInfo.push({ + name, + hash: plug.plugItemHash, + regex: new RegExp(name), + }); + } + } else { + // Stasis does not have a reusablePlug + name = + getDef( + 'InventoryItem', + item.sockets?.socketEntries[socket!.socketIndexes[0]].singleInitialItemHash, + ) + ?.displayProperties.name.toLowerCase() + .replace(/ - /g, '|') + .replace(/: /g, '|') ?? ''; + superInfo.push({ + name, + hash: item.sockets!.socketEntries[socket!.socketIndexes[0]].singleInitialItemHash, + regex: new RegExp(name), + }); + } + }); + + return superInfo; +} + +function getRegexByPCH(plugHashes: PlugCategoryHashes[]) { + const items = inventoryItems.filter((item) => + plugHashes.includes(item.plug?.plugCategoryHash ?? 0), + ); + const itemTypeDisplayName = items.flatMap((i) => i.itemTypeDisplayName.toLowerCase())[0]; + return RegExp( + `${items + .map((i) => i.displayProperties.name.toLowerCase()) + .join('|')}|${itemTypeDisplayName}(s)?`, + ); +} + +function getSuperRegex(obj: { name: string; hash: number; regex: RegExp }[]) { + const regex = [] as RegExp[]; + for (const [, value] of Object.entries(obj)) { + regex.push(Object(value).regex); + } + return getComposedRegex(...regex); +} + +function getBurnKeywords(burn: string) { + if (synergies[burn].superRegex) { + return getComposedRegex( + synergies[burn].superRegex!, + synergies[burn].grenades, + synergies[burn].melees, + synergies[burn].verbs, + synergies[burn].misc, + ); + } +} diff --git a/output/exotic-synergy.json b/output/exotic-synergy.json new file mode 100644 index 00000000..f51b9129 --- /dev/null +++ b/output/exotic-synergy.json @@ -0,0 +1,415 @@ +{ + "106575079": { + "damageType": [ + 4 + ], + "subclass": [ + 4260353952, + 4260353953 + ] + }, + "121305948": { + "damageType": [ + 2 + ], + "subclass": [ + 1081893461 + ] + }, + "136355432": { + "damageType": [ + 2, + 3, + 4 + ] + }, + "192896783": { + "damageType": [ + 7 + ] + }, + "193869523": { + "damageType": [ + 4 + ], + "subclass": [ + 2722573681, + 2722573683 + ] + }, + "235591051": { + "damageType": [ + 3 + ], + "subclass": [ + 2274196886 + ] + }, + "241462141": { + "damageType": [ + 4 + ], + "subclass": [ + 4260353952 + ] + }, + "300502917": { + "damageType": [ + 4 + ] + }, + "370930766": { + "damageType": [ + 3 + ], + "subclass": [ + 2274196886, + 2274196887 + ] + }, + "461841403": { + "damageType": [ + 4 + ] + }, + "475652357": { + "damageType": [ + 3 + ] + }, + "511888814": { + "damageType": [ + 4 + ] + }, + "691578979": { + "damageType": [ + 3 + ], + "subclass": [ + 375052471 + ] + }, + "903984858": { + "damageType": [ + 2 + ] + }, + "978537162": { + "damageType": [ + 3 + ] + }, + "1030017949": { + "damageType": [ + 4 + ], + "subclass": [ + 1656118681, + 1656118682 + ] + }, + "1053737370": { + "damageType": [ + 2 + ] + }, + "1192890598": { + "damageType": [ + 3 + ] + }, + "1321354573": { + "damageType": [ + 3 + ], + "subclass": [ + 375052468, + 375052469 + ] + }, + "1322544481": { + "damageType": [ + 6 + ] + }, + "1443166262": { + "damageType": [ + 4 + ] + }, + "1453120846": { + "damageType": [ + 3 + ] + }, + "1467044898": { + "damageType": [ + 6 + ] + }, + "1474735276": { + "damageType": [ + 4 + ], + "subclass": [ + 2722573682 + ] + }, + "1537074069": { + "damageType": [ + 3 + ] + }, + "1619425569": { + "damageType": [ + 6 + ] + }, + "1703551922": { + "damageType": [ + 2 + ], + "subclass": [ + 3769507633 + ] + }, + "1703598057": { + "damageType": [ + 2 + ] + }, + "1725917554": { + "damageType": [ + 2 + ] + }, + "1734844650": { + "damageType": [ + 3 + ] + }, + "1848640623": { + "damageType": [ + 4 + ], + "subclass": [ + 4260353952 + ] + }, + "1849149215": { + "damageType": [ + 2 + ] + }, + "1906093346": { + "damageType": [ + 4 + ] + }, + "1935198785": { + "damageType": [ + 4 + ] + }, + "1996008488": { + "damageType": [ + 2 + ], + "subclass": [ + 1081893460 + ] + }, + "2066430310": { + "damageType": [ + 3 + ], + "subclass": [ + 2747500760 + ] + }, + "2082483156": { + "damageType": [ + 3 + ] + }, + "2169905051": { + "damageType": [ + 6 + ] + }, + "2255796155": { + "damageType": [ + 2, + 3, + 4 + ] + }, + "2268523867": { + "damageType": [ + 2 + ], + "subclass": [ + 3769507633 + ] + }, + "2316914168": { + "damageType": [ + 3 + ], + "subclass": [ + 2274196886 + ] + }, + "2321120637": { + "damageType": [ + 2 + ], + "subclass": [ + 119041298 + ] + }, + "2384488862": { + "damageType": [ + 3 + ] + }, + "2415768376": { + "damageType": [ + 3 + ] + }, + "2463947681": { + "damageType": [ + 7 + ] + }, + "2563444729": { + "damageType": [ + 2 + ] + }, + "2757274117": { + "damageType": [ + 4 + ] + }, + "2766109872": { + "damageType": [ + 2 + ], + "subclass": [ + 3769507633 + ] + }, + "2773056939": { + "damageType": [ + 4 + ] + }, + "2808156426": { + "damageType": [ + 2 + ], + "subclass": [ + 119041299 + ] + }, + "3084282676": { + "damageType": [ + 2 + ] + }, + "3216110440": { + "damageType": [ + 2 + ] + }, + "3234692237": { + "damageType": [ + 4 + ] + }, + "3259193988": { + "damageType": [ + 6 + ] + }, + "3267996858": { + "damageType": [ + 4 + ] + }, + "3316517958": { + "damageType": [ + 3 + ] + }, + "3381022969": { + "damageType": [ + 2 + ], + "subclass": [ + 1081893460 + ] + }, + "3381022971": { + "damageType": [ + 4 + ] + }, + "3453042252": { + "damageType": [ + 3 + ] + }, + "3574051505": { + "damageType": [ + 6 + ] + }, + "3637722482": { + "damageType": [ + 7 + ] + }, + "3787517196": { + "damageType": [ + 3 + ] + }, + "3831935023": { + "damageType": [ + 6 + ], + "subclass": [ + 3683904166 + ] + }, + "3844826443": { + "damageType": [ + 6 + ] + }, + "3948284065": { + "damageType": [ + 4 + ], + "subclass": [ + 1656118680 + ] + }, + "4057299719": { + "damageType": [ + 3 + ], + "subclass": [ + 2274196887 + ] + }, + "4165919945": { + "damageType": [ + 2 + ] + } +} diff --git a/src/generate-exotic-armor-synergy.ts b/src/generate-exotic-armor-synergy.ts new file mode 100644 index 00000000..104408e6 --- /dev/null +++ b/src/generate-exotic-armor-synergy.ts @@ -0,0 +1,153 @@ +import { getDef, getAllDefs } from '@d2api/manifest-node'; +import { burns, synergies } from '../data/exotic-synergies.js'; +import { writeFile, sortWithoutArticles } from './helpers.js'; +import { DamageType } from 'bungie-api-ts/destiny2/interfaces.js'; + +const debug = true; + +const inventoryItems = getAllDefs('InventoryItem'); +const exoticSynergy = {} as Record; +const exoticSocketTypeHash = 965959289; + +const debugArc: string[] = []; +const debugSolar: string[] = []; +const debugVoid: string[] = []; +const debugStasis: string[] = []; +const debugStrand: string[] = []; +const debugNeutral: string[] = []; + +inventoryItems.filter( + (item) => + item.equippingBlock?.uniqueLabel === 'exotic_armor' && + item.sockets?.socketEntries.find((socket) => { + if (socket.socketTypeHash === exoticSocketTypeHash) { + const synergy = [] as string[]; + const damageType = [] as number[]; + const subclass = [] as number[]; + const intrinsicTraitDescription = + getDef( + 'InventoryItem', + socket.singleInitialItemHash, + )?.displayProperties.description.toLowerCase() ?? ''; + + for (const burn of burns) { + if ( + synergies[burn].keywords.include?.test(intrinsicTraitDescription) && + !synergies[burn].keywords.exclude?.test(intrinsicTraitDescription) + ) { + damageType.push(synergies[burn].damageType); + if (debug) { + synergy.push( + synergyDebugInfo( + `${item.displayProperties.name}: ${intrinsicTraitDescription.replace(/\n/g, '')}`, + synergies[burn].damageType, + ), + ); + } + for (const sooper of synergies[burn].super) { + if (sooper.regex.test(intrinsicTraitDescription)) { + subclass.push(sooper.hash); + if (debug) { + synergy.push(sooper.name); + } + } + } + subclass.sort(); + } + } + + // if an exotic matches all subclass damageTypes it is a neutral exotic + if (damageType.length === 5) { + damageType.length = 0; + if (debug) { + debugArc.pop(); + debugSolar.pop(); + debugVoid.pop(); + debugStasis.pop(); + debugStrand.pop(); + } + } + + if (damageType.length > 0) { + if (!exoticSynergy[item.hash]) { + exoticSynergy[item.hash] = {}; + } + exoticSynergy[item.hash].damageType = damageType; + } + + if (subclass.length > 0) { + if (!exoticSynergy[item.hash]) { + exoticSynergy[item.hash] = {}; + } + exoticSynergy[item.hash].subclass = subclass; + } + + if (debug) { + if (damageType.length === 0 && subclass.length === 0) { + debugNeutral.push( + `${item.displayProperties.name}: ${intrinsicTraitDescription.replace(/\n/g, '')}`, + ); + } + } + } + }), +); + +if (debug) { + console.log(synergies); + // Generate MarkDown for easy pasting into Github + console.log( + `# ${debugArc.length} Arc Exotics\n \`\`\`\n`, + debugArc.sort(sortWithoutArticles), + `\n\`\`\``, + ); + console.log( + `# ${debugSolar.length} Solar Exotics\n \`\`\`\n`, + debugSolar.sort(sortWithoutArticles), + `\n\`\`\``, + ); + console.log( + `# ${debugVoid.length} Void Exotics\n \`\`\`\n`, + debugVoid.sort(sortWithoutArticles), + `\n\`\`\``, + ); + console.log( + `# ${debugStasis.length} Stasis Exotics\n \`\`\`\n`, + debugStasis.sort(sortWithoutArticles), + `\n\`\`\``, + ); + console.log( + `# ${debugStrand.length} Strand Exotics\n \`\`\`\n`, + debugStrand.sort(sortWithoutArticles), + `\n\`\`\``, + ); + console.log( + `# ${debugNeutral.length} Neutral Exotics\n \`\`\`\n`, + debugNeutral.sort(sortWithoutArticles), + `\n\`\`\``, + ); +} + +writeFile('./output/exotic-synergy.json', exoticSynergy); + +function synergyDebugInfo(name: string, damageType: number) { + switch (damageType) { + case DamageType.Arc: + debugArc.push(name); + return 'arc'; + case DamageType.Thermal: + debugSolar.push(name); + return 'solar'; + case DamageType.Void: + debugVoid.push(name); + return 'void'; + case DamageType.Stasis: + debugStasis.push(name); + return 'stasis'; + case DamageType.Strand: + debugStrand.push(name); + return 'strand'; + default: + return ''; + } +} diff --git a/src/generate-extended-breaker.ts b/src/generate-extended-breaker.ts index 4fb9f30f..2ecc5a67 100644 --- a/src/generate-extended-breaker.ts +++ b/src/generate-extended-breaker.ts @@ -5,13 +5,16 @@ import { writeFile } from './helpers.js'; const inventoryItems = getAllDefs('InventoryItem'); const extendedBreakers: Record = {}; +const exoticSocketTypeHash = 965959289; inventoryItems.filter( (item) => item.equippingBlock && item.breakerType === 0 && item.sockets?.socketEntries.find((socket) => { - if ([SocketCategoryHashes.IntrinsicTraits, 965959289].includes(socket.socketTypeHash)) { + if ( + [SocketCategoryHashes.IntrinsicTraits, exoticSocketTypeHash].includes(socket.socketTypeHash) + ) { let extendedBreaker = 0; const intrinsicTraitDescription = getDef( diff --git a/src/helpers.ts b/src/helpers.ts index 60ee202b..61dfab40 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -165,3 +165,32 @@ export function getCurrentSeason() { } return 0; } + +export function getComposedRegex(...regexes: RegExp[]) { + return new RegExp(regexes.map((regex) => regex.source).join('|')); +} + +export function sortWithoutArticles(a: string, b: string) { + const aTitle = removeArticles(a.toLowerCase()); + const bTitle = removeArticles(b.toLowerCase()); + + if (aTitle > bTitle) { + return 1; + } + if (aTitle < bTitle) { + return -1; + } + return 0; +} + +function removeArticles(str: string) { + const articles = ['a', 'an', 'the']; + const words = str.split(' '); + if (words.length <= 1) { + return str; + } + if (articles.includes(words[0])) { + return words.splice(1).join(' '); + } + return str; +}