Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 17 additions & 6 deletions src/calculateTreeQuantity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@ import { RecipeTreeWithCraftFlags, RecipeTree, RecipeTreeWithQuantity } from './
export function calculateTreeQuantity(
amount: number,
tree: RecipeTreeWithCraftFlags,
availableItems?: Record<string, number>
availableItems?: Record<string, number>,
ignoredBitItemIds?: Array<number>
): RecipeTreeWithCraftFlags
export function calculateTreeQuantity(
amount: number,
tree: RecipeTree,
availableItems?: Record<string, number>
availableItems?: Record<string, number>,
ignoredBitItemIds?: Array<number>
): RecipeTreeWithQuantity
export function calculateTreeQuantity(
amount: number,
tree: RecipeTree | RecipeTreeWithCraftFlags,
availableItems: Record<string, number> = {}
availableItems: Record<string, number> = {},
ignoredBitItemIds: Array<number> = []
) {
// 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
Expand All @@ -28,13 +33,18 @@ function calculateTreeQuantityInner(
tree: RecipeTree | RecipeTreeWithCraftFlags,
availableItems: Record<string, number>,
ignoreAvailable = false,
nesting = 0
nesting = 0,
ignoredBitItemIds: Array<number>
): 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)
Expand Down Expand Up @@ -69,7 +79,8 @@ function calculateTreeQuantityInner(
component,
availableItems,
ignoreAvailable,
++nesting
++nesting,
ignoredBitItemIds
)
})

Expand Down
136 changes: 96 additions & 40 deletions src/cheapestTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ export function cheapestTree(
'103049': '0',
}
): RecipeTreeWithCraftFlags {
tree = applyEfficiencyTiersToTree(tree, userEfficiencyTiers)
const ignoredBitItemIds: Array<number> = []
tree = initialTreeChecks(tree, userEfficiencyTiers, ignoredBitItemIds)
if (valueOwnItems) {
const treeWithQuantityWithoutAvailableItems = calculateTreeQuantity(
amount,
tree as RecipeTree,
{}
{},
ignoredBitItemIds
)
const treeWithPriceWithoutAvailableItems = calculateTreePrices(
treeWithQuantityWithoutAvailableItems,
Expand All @@ -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)
Expand All @@ -54,7 +61,8 @@ export function cheapestTree(
const treeWithQuantityPostFlags = calculateTreeQuantity(
amount,
treeWithCraftFlags,
availableItems
availableItems,
ignoredBitItemIds
)

// Recalculate the correct tree price
Expand Down Expand Up @@ -108,57 +116,105 @@ function disableCraftForItemIds(
return tree
}

export function initialTreeChecks(
tree: NestedRecipe,
userEfficiencyTiers: Record<string, string>,
ignoredBitItemIds: Array<number>,
bitItemIds = new Set<number>(),
normalItemIds = new Set<number>(),
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<number>,
normalItemIds: Set<number>
) {
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<NestedRecipe, 'id'> & { id: number | null }, // FIXME Not sure why this can be null
tree: NestedRecipe,
userEfficiencyTiers: Record<string, string>
): 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
}
111 changes: 111 additions & 0 deletions tests/calculateTreeQuantity.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<number> = []
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<Component & { components: Array<Component> }>

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)
})
})
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Loading