initial commit

This commit is contained in:
Jo Blo
2026-03-04 17:01:08 -05:00
commit 39d61d797d
51 changed files with 7864 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
import { computed } from 'vue'
import type { Ref } from 'vue'
import type { CraftingRecipe, ProfitResult, IngredientBreakdown, SortState } from '../types/crafting'
import type { FilterState } from '../types/filters'
import { useAlbionPrices } from './useAlbionPrices'
import { formatItemId } from '../utils/formatting'
// Returns 0=basic, 1=artifact, 2=avalon, 3=crystal
function variantRank(outputItemId: string): number {
const id = outputItemId.replace(/@\d$/, '') // strip enchantment suffix
if (id.endsWith('_AVALON')) return 2
if (id.endsWith('_CRYSTAL')) return 3
if (/_SET[123]$/.test(id)) return 0
// Artifact suffixes: UNDEAD, HELL, MORGANA, KEEPER, and unique named artifacts
if (/_(?:UNDEAD|HELL|MORGANA|KEEPER|ROYAL|FEY)$/.test(id)) return 1
return 0
}
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 rrrFactor = 1 - f.rrr / 100
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
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 materialCost = 0
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
materialCost += totalCost
ingredientBreakdown.push({
itemId: ing.itemId,
displayName: formatItemId(ing.itemId),
quantity: ing.quantity,
unitPrice,
totalCost,
})
}
}
const effectiveMaterialCost = materialCost * rrrFactor
const priceAgeMs = missingPrices ? null : (oldestDate ? Date.now() - new Date(oldestDate).getTime() : null)
results.push({
recipe,
materialCost,
effectiveMaterialCost,
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 }
}