diff --git a/package.json b/package.json index d1f15ce..c08d636 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@devoxa/eslint-config": "2.0.5", "@devoxa/flocky": "^1.3.1", "@devoxa/prettier-config": "1.0.0", - "@gw2efficiency/recipe-nesting": "^3.3.0", + "@gw2efficiency/recipe-nesting": "^3.4.0", "@types/jest": "27.0.1", "@types/node": "15.12.5", "eslint": "7.32.0", diff --git a/src/calculateTreeQuantity.ts b/src/calculateTreeQuantity.ts index 1eb07c0..0947317 100755 --- a/src/calculateTreeQuantity.ts +++ b/src/calculateTreeQuantity.ts @@ -3,22 +3,27 @@ import { RecipeTreeWithCraftFlags, RecipeTree, RecipeTreeWithQuantity } from './ export function calculateTreeQuantity( amount: number, tree: RecipeTreeWithCraftFlags, - availableItems?: Record + availableItems?: Record, + ignoredBitItemIds?: Array ): RecipeTreeWithCraftFlags export function calculateTreeQuantity( amount: number, tree: RecipeTree, - availableItems?: Record + availableItems?: Record, + ignoredBitItemIds?: Array ): RecipeTreeWithQuantity export function calculateTreeQuantity( amount: number, tree: RecipeTree | RecipeTreeWithCraftFlags, - availableItems: Record = {} + availableItems: Record = {}, + ignoredBitItemIds: Array = [] ) { // Make sure that we don't modify the passed-in object // We still want to work with a reference in the actual calculation // since the availableItems are a shared state for all sub-recipes - return calculateTreeQuantityInner(amount, tree, { ...availableItems }) + return calculateTreeQuantityInner(amount, tree, { ...availableItems }, false, 0, [ + ...ignoredBitItemIds, + ]) } // Go through a recipe tree and set 'totalQuantity' based on the @@ -28,13 +33,18 @@ function calculateTreeQuantityInner( tree: RecipeTree | RecipeTreeWithCraftFlags, availableItems: Record, ignoreAvailable = false, - nesting = 0 + nesting = 0, + ignoredBitItemIds: Array ): RecipeTreeWithCraftFlags | RecipeTreeWithQuantity { const output = tree.output || 1 // Calculate the total quantity needed let treeQuantity = amount * tree.quantity + if (typeof tree.achievement_bit === 'number') { + ignoredBitItemIds.includes(tree.id) ? (treeQuantity = 0) : ignoredBitItemIds.push(tree.id) + } + // Round amount to nearest multiple of the tree output treeQuantity = Math.ceil(treeQuantity / output) * output const totalQuantity = Math.round(treeQuantity) @@ -69,7 +79,8 @@ function calculateTreeQuantityInner( component, availableItems, ignoreAvailable, - ++nesting + ++nesting, + ignoredBitItemIds ) }) diff --git a/src/cheapestTree.ts b/src/cheapestTree.ts index 9a24915..7f1de46 100755 --- a/src/cheapestTree.ts +++ b/src/cheapestTree.ts @@ -21,12 +21,14 @@ export function cheapestTree( '103049': '0', } ): RecipeTreeWithCraftFlags { - tree = applyEfficiencyTiersToTree(tree, userEfficiencyTiers) + const ignoredBitItemIds: Array = [] + tree = initialTreeChecks(tree, userEfficiencyTiers, ignoredBitItemIds) if (valueOwnItems) { const treeWithQuantityWithoutAvailableItems = calculateTreeQuantity( amount, tree as RecipeTree, - {} + {}, + ignoredBitItemIds ) const treeWithPriceWithoutAvailableItems = calculateTreePrices( treeWithQuantityWithoutAvailableItems, @@ -40,7 +42,12 @@ export function cheapestTree( } // Adjust the tree total and used quantities - const treeWithQuantity = calculateTreeQuantity(amount, tree as RecipeTree, availableItems) + const treeWithQuantity = calculateTreeQuantity( + amount, + tree as RecipeTree, + availableItems, + ignoredBitItemIds + ) // Set the initial craft flags based on the subtree prices const treeWithPrices = calculateTreePrices(treeWithQuantity, itemPrices) @@ -54,7 +61,8 @@ export function cheapestTree( const treeWithQuantityPostFlags = calculateTreeQuantity( amount, treeWithCraftFlags, - availableItems + availableItems, + ignoredBitItemIds ) // Recalculate the correct tree price @@ -108,57 +116,105 @@ function disableCraftForItemIds( return tree } +export function initialTreeChecks( + tree: NestedRecipe, + userEfficiencyTiers: Record, + ignoredBitItemIds: Array, + bitItemIds = new Set(), + normalItemIds = new Set(), + isRootNode = true +): NestedRecipe { + collectItemDataForIgnoringBits(tree, bitItemIds, normalItemIds) + tree = applyEfficiencyTiersToTree(tree, userEfficiencyTiers) + + if ('components' in tree && Array.isArray(tree.components)) { + tree = { + ...tree, + components: tree.components.map((component) => + initialTreeChecks( + component as NestedRecipe, + userEfficiencyTiers, + ignoredBitItemIds, + bitItemIds, + normalItemIds, + false + ) + ), + } + } + + if (isRootNode) { + bitItemIds.forEach((id) => { + if (normalItemIds.has(id)) { + ignoredBitItemIds.push(id) + } + }) + } + + return tree +} + +function collectItemDataForIgnoringBits( + tree: NestedRecipe, + bitItemIds: Set, + normalItemIds: Set +) { + if (!tree.id) return + if ((tree.type as 'Recipe' | 'Currency' | 'Item') === 'Currency') return + + if (typeof tree.achievement_bit === 'number') { + bitItemIds.add(tree.id) + } else { + normalItemIds.add(tree.id) + } +} + function applyEfficiencyTiersToTree( - tree: Omit & { id: number | null }, // FIXME Not sure why this can be null + tree: NestedRecipe, userEfficiencyTiers: Record ): NestedRecipe { + if (!tree.id) return tree const id = tree.id ? tree.id.toString() : '' if ( - ['102306', '102205', '103049'].includes(id) && - tree.merchant && - tree.merchant.name.includes('Homestead Refinement') + !['102306', '102205', '103049'].includes(id) || + !tree.merchant || + !tree.merchant.name.includes('Homestead Refinement') ) { - const efficiencyTier = Number(userEfficiencyTiers[id]) - - if (efficiencyTier > 0) { - const component = { ...tree.components[0] } + return tree + } - // Each efficiency tier lowers input by 50%, if it drops below one then doubles output - component.quantity = component.quantity / (efficiencyTier * 2) + const efficiencyTier = Number(userEfficiencyTiers[id]) + if (!(efficiencyTier > 0)) return tree - // Bug: Onions are discounted by 75% with first tier - if (component.id === 12142) { - component.quantity = efficiencyTier === 1 ? 1 : 0.5 - } + const component = { ...tree.components[0] } - // Bug: Potatoes are not discounted with first tier - if (component.id === 12135) { - component.quantity = efficiencyTier === 1 ? 8 : 4 - } + // Each efficiency tier lowers input by 50%, if it drops below one then doubles output + component.quantity = component.quantity / (efficiencyTier * 2) - let updatedTree = { ...tree, output: component.quantity < 1 ? tree.output * 2 : tree.output } - - // Bug: Iron ore output also halves with second tier - if (component.id === 19699 && efficiencyTier === 2) { - updatedTree.output = updatedTree.output / 2 - } + // Bug: Onions are discounted by 75% with first tier + if (component.id === 12142) { + component.quantity = efficiencyTier === 1 ? 1 : 0.5 + } - component.quantity = component.quantity < 1 ? 1 : component.quantity - updatedTree = { ...updatedTree, components: [component, ...tree.components.slice(1)] } + // Bug: Potatoes are not discounted with first tier + if (component.id === 12135) { + component.quantity = efficiencyTier === 1 ? 8 : 4 + } - tree = updatedTree - } + let updatedTree = { + ...tree, + output: component.quantity < 1 ? tree.output * 2 : tree.output, } - if ('components' in tree && Array.isArray(tree.components)) { - tree = { - ...tree, - components: tree.components.map((component) => - applyEfficiencyTiersToTree(component as NestedRecipe, userEfficiencyTiers) - ), - } + // Bug: Iron ore output also halves with second tier + if (component.id === 19699 && efficiencyTier === 2) { + updatedTree.output = updatedTree.output / 2 } - return tree as NestedRecipe + component.quantity = component.quantity < 1 ? 1 : component.quantity + updatedTree = { ...updatedTree, components: [component, ...tree.components.slice(1)] } + tree = updatedTree + + return tree } diff --git a/tests/calculateTreeQuantity.spec.ts b/tests/calculateTreeQuantity.spec.ts index e211bc9..08e71bb 100755 --- a/tests/calculateTreeQuantity.spec.ts +++ b/tests/calculateTreeQuantity.spec.ts @@ -1,5 +1,7 @@ +import { initialTreeChecks } from '../src/cheapestTree' import { calculateTreeQuantity } from '../src/calculateTreeQuantity' import { RecipeTree, RecipeTreeWithCraftFlags } from '../src/types' +import { NestedRecipe } from '@gw2efficiency/recipe-nesting' const RECIPE_PARTIAL = { id: 1, @@ -381,4 +383,113 @@ describe('calculateTreeQuantity (used quantity)', () => { expect(calculateTreeQuantity(1, recipeTree, availableItems)).toMatchSnapshot() }) + + it('handles achievement bit items correctly', () => { + const recipeTree: RecipeTree = { + ...RECIPE_PARTIAL, + id: 100, + quantity: 1, + output: 1, + components: [ + { + ...ITEM_PARTIAL, + id: 55, + quantity: 1, + output: 1, + achievement_bit: 0, + }, + { + ...RECIPE_PARTIAL, + id: 200, + quantity: 1, + output: 1, + components: [ + { + ...ITEM_PARTIAL, + id: 55, + quantity: 1, + output: 1, + achievement_bit: 0, + }, + ], + }, + { + ...ITEM_PARTIAL, + id: 55, + quantity: 2, + output: 1, + }, + { + ...ITEM_PARTIAL, + id: 56, + quantity: 1, + output: 1, + achievement_bit: 1, + }, + { + ...ITEM_PARTIAL, + id: 56, + quantity: 1, + output: 1, + achievement_bit: 1, + }, + { + ...ITEM_PARTIAL, + id: 999, + quantity: 1, + output: 1, + }, + { + ...ITEM_PARTIAL, + id: 999, + quantity: 3, + output: 1, + }, + ], + } + + const ignoredBitItemIds: Array = [] + initialTreeChecks( + recipeTree as unknown as NestedRecipe, + { '102306': '0', '102205': '0', '103049': '0' }, + ignoredBitItemIds + ) + + const adjusted = calculateTreeQuantity(1, recipeTree, {}, ignoredBitItemIds) + + type Component = { totalQuantity: number; usedQuantity: 0 } + const [ + firstBitTopComponent, + firstBitComponentWithNestedBit, + normalItemWithFirstBitItemId, + secondBitTopComponent, + secondBitTopComponentDuplicate, + normalItemWithNoBitVersion, + normalItemWithNoBitVersionDuplicate, + ] = adjusted.components as Array }> + + const firstBitInsideComponent = firstBitComponentWithNestedBit.components[0] + + // Bit exists as real item elsewhere, zeroed + expect(firstBitTopComponent.totalQuantity).toBe(0) + expect(firstBitTopComponent.usedQuantity).toBe(0) + + // Bit exists as real item elsewhere, zeroed in deeper nesting + expect(firstBitInsideComponent.totalQuantity).toBe(0) + expect(firstBitInsideComponent.usedQuantity).toBe(0) + + // Real item is not zeroed when bit version exists + expect(normalItemWithFirstBitItemId.totalQuantity).toBe(2) + expect(normalItemWithFirstBitItemId.usedQuantity).toBe(2) + + // Duplicate bit items, first one is kept + expect(secondBitTopComponent.totalQuantity).toBe(1) + + // Duplicate bit items, second one is zeroed + expect(secondBitTopComponentDuplicate.totalQuantity).toBe(0) + + // Real duplicate items are unaffected + expect(normalItemWithNoBitVersion.totalQuantity).toBe(1) + expect(normalItemWithNoBitVersionDuplicate.totalQuantity).toBe(3) + }) }) diff --git a/yarn.lock b/yarn.lock index 106c9ca..6ddcc3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -335,10 +335,10 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@gw2efficiency/recipe-nesting@^3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@gw2efficiency/recipe-nesting/-/recipe-nesting-3.3.0.tgz#4d97dcb2e2c126cdcb1c93afb9cfed28d027ece4" - integrity sha512-tbbO/0XNyHeSVCDnYN0dNxRYeOWMN0MDoEqjyEqxMQ36EuchR0D1qgcHGA56nNc7gI9okfLhxZWslTB7kuivmw== +"@gw2efficiency/recipe-nesting@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@gw2efficiency/recipe-nesting/-/recipe-nesting-3.4.0.tgz#43eeea9e43b44aaa24d8f9e7881e095796b33453" + integrity sha512-p4y8EmJpX9A5jNk6MhUwHTvESFnrjzfsKW5DpGthQVVUvMWRlammo6LG7mIptAeHKuHf8TcAXTf8FvxThe/B+w== dependencies: "@devoxa/flocky" "^1.3.1"