147 lines
5.0 KiB
TypeScript
147 lines
5.0 KiB
TypeScript
import { computed } from 'vue'
|
|
import type { Ref } from 'vue'
|
|
import type { CraftingRecipe, ProfitResult, IngredientBreakdown, SortState } from '../types/crafting'
|
|
import type { FilterState, VariantType } from '../types/filters'
|
|
import { useAlbionPrices } from './useAlbionPrices'
|
|
import { formatItemId } from '../utils/formatting'
|
|
import { localProductionBonus, rrrFromBonus, isRrrExempt, FOCUS_LPB } from '../data/cityBonuses'
|
|
|
|
function variantOf(outputItemId: string): VariantType {
|
|
const id = outputItemId.replace(/@\d$/, '') // strip enchantment suffix
|
|
if (id.endsWith('_AVALON')) return 'avalon'
|
|
if (id.endsWith('_CRYSTAL')) return 'crystal'
|
|
if (/_SET[123]$/.test(id)) return 'basic'
|
|
if (/_(?:UNDEAD|HELL|MORGANA|KEEPER|ROYAL|FEY)$/.test(id)) return 'artifact'
|
|
return 'basic'
|
|
}
|
|
|
|
const VARIANT_ORDER: Record<VariantType, number> = { basic: 0, artifact: 1, avalon: 2, crystal: 3 }
|
|
|
|
function variantRank(outputItemId: string): number {
|
|
return VARIANT_ORDER[variantOf(outputItemId)]
|
|
}
|
|
|
|
export function useCraftingProfit(
|
|
recipes: CraftingRecipe[],
|
|
filters: Ref<FilterState>,
|
|
sortState: Ref<SortState>
|
|
) {
|
|
const { getPrice } = useAlbionPrices()
|
|
|
|
const profitResults = computed<ProfitResult[]>(() => {
|
|
const f = filters.value
|
|
const city = f.city
|
|
|
|
const results: ProfitResult[] = []
|
|
const nameLower = f.nameFilter.trim().toLowerCase()
|
|
|
|
for (const recipe of recipes) {
|
|
if (!f.tiers.has(recipe.tier)) continue
|
|
if (f.enchantments !== null && !f.enchantments.has(recipe.enchantment)) continue
|
|
if (f.variants !== null && !f.variants.has(variantOf(recipe.outputItemId))) continue
|
|
const baseName = recipe.displayName.replace(/^T\d+\.\d /, '')
|
|
if (f.selectedItemTypes !== null && !f.selectedItemTypes.has(baseName)) continue
|
|
if (nameLower && !recipe.displayName.toLowerCase().includes(nameLower)) continue
|
|
|
|
// Calculate material cost from ingredients only
|
|
let basicCost = 0 // resources subject to RRR
|
|
let artefactCost = 0 // artefacts / ALCHEMY_RARE — not subject to RRR
|
|
let missingPrices = false
|
|
|
|
// Track oldest price date (for status column)
|
|
let oldestDate: string | null = null
|
|
function trackDate(date: string) {
|
|
if (!oldestDate || date < oldestDate) oldestDate = date
|
|
}
|
|
|
|
const ingredientBreakdown: IngredientBreakdown[] = []
|
|
|
|
for (const ing of recipe.ingredients) {
|
|
const ingEntry = getPrice(ing.itemId, city)
|
|
const unitPrice = ingEntry?.sell_price_min ?? 0
|
|
if (ingEntry === null || unitPrice === 0) {
|
|
missingPrices = true
|
|
ingredientBreakdown.push({
|
|
itemId: ing.itemId,
|
|
displayName: formatItemId(ing.itemId),
|
|
quantity: ing.quantity,
|
|
unitPrice: 0,
|
|
totalCost: 0,
|
|
})
|
|
} else {
|
|
trackDate(ingEntry.sell_price_min_date)
|
|
const totalCost = unitPrice * ing.quantity
|
|
if (isRrrExempt(ing.itemId)) artefactCost += totalCost
|
|
else basicCost += totalCost
|
|
ingredientBreakdown.push({
|
|
itemId: ing.itemId,
|
|
displayName: formatItemId(ing.itemId),
|
|
quantity: ing.quantity,
|
|
unitPrice,
|
|
totalCost,
|
|
})
|
|
}
|
|
}
|
|
|
|
const materialCost = basicCost + artefactCost
|
|
const lpb = localProductionBonus(city, recipe.outputItemId)
|
|
const rrr = rrrFromBonus(lpb)
|
|
const rrrFocus = rrrFromBonus(lpb + FOCUS_LPB)
|
|
const effectiveMaterialCost = basicCost * (1 - rrr / 100) + artefactCost
|
|
const effectiveMaterialCostFocus = basicCost * (1 - rrrFocus / 100) + artefactCost
|
|
const priceAgeMs = missingPrices ? null : (oldestDate ? Date.now() - new Date(oldestDate).getTime() : null)
|
|
|
|
results.push({
|
|
recipe,
|
|
materialCost,
|
|
effectiveMaterialCost,
|
|
effectiveMaterialCostFocus,
|
|
rrr,
|
|
rrrFocus,
|
|
priceAgeMs,
|
|
missingPrices,
|
|
ingredientBreakdown,
|
|
})
|
|
}
|
|
|
|
// Sort results
|
|
const { field, direction } = sortState.value
|
|
results.sort((a, b) => {
|
|
let aVal: number | string
|
|
let bVal: number | string
|
|
|
|
switch (field) {
|
|
case 'materialCost':
|
|
aVal = a.effectiveMaterialCost
|
|
bVal = b.effectiveMaterialCost
|
|
break
|
|
case 'displayName':
|
|
aVal = a.recipe.displayName
|
|
bVal = b.recipe.displayName
|
|
break
|
|
case 'tier':
|
|
aVal = a.recipe.tier
|
|
bVal = b.recipe.tier
|
|
break
|
|
case 'variantType':
|
|
aVal = variantRank(a.recipe.outputItemId)
|
|
bVal = variantRank(b.recipe.outputItemId)
|
|
break
|
|
default:
|
|
aVal = a.effectiveMaterialCost
|
|
bVal = b.effectiveMaterialCost
|
|
}
|
|
|
|
if (typeof aVal === 'string' && typeof bVal === 'string') {
|
|
return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal)
|
|
}
|
|
const diff = (aVal as number) - (bVal as number)
|
|
return direction === 'asc' ? diff : -diff
|
|
})
|
|
|
|
return results
|
|
})
|
|
|
|
return { profitResults }
|
|
}
|