Files
albion-crafting-calc/src/composables/useCraftingProfit.ts

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 }
}