add local production bonuses
This commit is contained in:
@@ -18,9 +18,9 @@
|
|||||||
<!-- Calculator page -->
|
<!-- Calculator page -->
|
||||||
<div v-if="currentPage === 'calculator'" class="p-6 space-y-4">
|
<div v-if="currentPage === 'calculator'" class="p-6 space-y-4">
|
||||||
<FilterBar :filters="filters" :result-count="profitResults.length" @set-name-filter="setNameFilter"
|
<FilterBar :filters="filters" :result-count="profitResults.length" @set-name-filter="setNameFilter"
|
||||||
@set-city="setCity" @toggle-tier="toggleTier" @set-selected-item-types="setSelectedItemTypes"
|
@toggle-tier="toggleTier" @set-selected-item-types="setSelectedItemTypes"
|
||||||
@toggle-enchantment="toggleEnchantment" @reset-enchantments="resetEnchantments"
|
@toggle-enchantment="toggleEnchantment" @reset-enchantments="resetEnchantments"
|
||||||
@toggle-variant="toggleVariant" @reset-variants="resetVariants" @set-rrr="setRrr" />
|
@toggle-variant="toggleVariant" @reset-variants="resetVariants" />
|
||||||
|
|
||||||
<ProfitTable :results="profitResults" :sort-state="sortState" @sort="handleSort" />
|
<ProfitTable :results="profitResults" :sort-state="sortState" @sort="handleSort" />
|
||||||
</div>
|
</div>
|
||||||
@@ -66,10 +66,8 @@ const sortState = ref<SortState>({ field: 'materialCost', direction: 'asc' })
|
|||||||
// Filters
|
// Filters
|
||||||
const {
|
const {
|
||||||
filters,
|
filters,
|
||||||
setCity,
|
|
||||||
toggleTier,
|
toggleTier,
|
||||||
setSelectedItemTypes,
|
setSelectedItemTypes,
|
||||||
setRrr,
|
|
||||||
setNameFilter,
|
setNameFilter,
|
||||||
toggleEnchantment,
|
toggleEnchantment,
|
||||||
resetEnchantments,
|
resetEnchantments,
|
||||||
|
|||||||
@@ -131,29 +131,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- City select -->
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<span class="text-xs text-gray-400">City</span>
|
|
||||||
<select
|
|
||||||
class="bg-gray-900 border border-gray-700 rounded-lg px-2 py-1.5 text-sm text-gray-200 focus:outline-none focus:border-amber-500 cursor-pointer"
|
|
||||||
:value="filters.city" @change="$emit('set-city', ($event.target as HTMLSelectElement).value as AlbionCity)">
|
|
||||||
<option v-for="c in CITIES" :key="c" :value="c">{{ c }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="h-5 w-px bg-gray-600" />
|
|
||||||
|
|
||||||
<!-- RRR -->
|
|
||||||
<div class="flex items-center gap-1.5">
|
|
||||||
<span class="text-xs text-gray-400 whitespace-nowrap">RRR</span>
|
|
||||||
<input type="number"
|
|
||||||
class="bg-gray-900 border border-gray-700 rounded-lg px-2 py-1.5 text-sm text-gray-200 w-16 focus:outline-none focus:border-amber-500 font-mono"
|
|
||||||
:value="filters.rrr" @input="$emit('set-rrr', Number(($event.target as HTMLInputElement).value))" min="0"
|
|
||||||
max="100" step="0.5" />
|
|
||||||
<span class="text-xs text-gray-500">%</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Result count -->
|
<!-- Result count -->
|
||||||
<span class="ml-auto text-xs text-gray-500 whitespace-nowrap">{{ resultCount }} results</span>
|
<span class="ml-auto text-xs text-gray-500 whitespace-nowrap">{{ resultCount }} results</span>
|
||||||
|
|
||||||
@@ -166,9 +143,8 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|||||||
import { tierStyle, enchantStyle } from '../../utils/formatting'
|
import { tierStyle, enchantStyle } from '../../utils/formatting'
|
||||||
import type { FilterState, VariantType } from '../../types/filters'
|
import type { FilterState, VariantType } from '../../types/filters'
|
||||||
import { ALL_VARIANTS } from '../../types/filters'
|
import { ALL_VARIANTS } from '../../types/filters'
|
||||||
import type { AlbionCity } from '../../types/api'
|
|
||||||
import type { Tier, Enchantment } from '../../types/crafting'
|
import type { Tier, Enchantment } from '../../types/crafting'
|
||||||
import { TIERS, CITIES, ENCHANTMENTS } from '../../data/constants'
|
import { TIERS, ENCHANTMENTS } from '../../data/constants'
|
||||||
import { ITEM_TREE, ALL_ITEM_NAMES, getLeaves } from '../../data/itemTree'
|
import { ITEM_TREE, ALL_ITEM_NAMES, getLeaves } from '../../data/itemTree'
|
||||||
import type { TreeNode } from '../../data/itemTree'
|
import type { TreeNode } from '../../data/itemTree'
|
||||||
|
|
||||||
@@ -179,14 +155,12 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'set-name-filter': [value: string]
|
'set-name-filter': [value: string]
|
||||||
'set-city': [city: AlbionCity]
|
|
||||||
'toggle-tier': [tier: Tier]
|
'toggle-tier': [tier: Tier]
|
||||||
'set-selected-item-types': [v: Set<string> | null]
|
'set-selected-item-types': [v: Set<string> | null]
|
||||||
'toggle-enchantment': [enc: Enchantment]
|
'toggle-enchantment': [enc: Enchantment]
|
||||||
'reset-enchantments': []
|
'reset-enchantments': []
|
||||||
'toggle-variant': [v: VariantType]
|
'toggle-variant': [v: VariantType]
|
||||||
'reset-variants': []
|
'reset-variants': []
|
||||||
'set-rrr': [rate: number]
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// ─── Category tree ────────────────────────────────────────────────────────────
|
// ─── Category tree ────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -9,12 +9,28 @@
|
|||||||
{{ totalRecipes }} recipes
|
{{ totalRecipes }} recipes
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- City selector -->
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="text-xs text-gray-400">City</span>
|
||||||
|
<select
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg px-2 py-1.5 text-sm text-gray-200 focus:outline-none focus:border-amber-500 cursor-pointer"
|
||||||
|
:value="filters.city"
|
||||||
|
@change="setCity(($event.target as HTMLSelectElement).value as AlbionCity)"
|
||||||
|
>
|
||||||
|
<option v-for="c in CITIES" :key="c" :value="c">{{ c }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
import { useFilters } from '../../composables/useFilters'
|
||||||
totalRecipes: number
|
import { CITIES } from '../../data/constants'
|
||||||
}>()
|
import type { AlbionCity } from '../../types/api'
|
||||||
|
|
||||||
|
defineProps<{ totalRecipes: number }>()
|
||||||
|
|
||||||
|
const { filters, setCity } = useFilters()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -185,7 +185,7 @@
|
|||||||
<span class="text-gray-200">{{ formatSilver(result.materialCost) }}</span>
|
<span class="text-gray-200">{{ formatSilver(result.materialCost) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between gap-4">
|
<div class="flex justify-between gap-4">
|
||||||
<span class="text-gray-400">RRR ({{ filters.rrr }}%)</span>
|
<span class="text-gray-400">RRR ({{ result.rrr.toFixed(1) }}%)</span>
|
||||||
<span class="text-green-400">−{{ formatSilver(result.materialCost - result.effectiveMaterialCost) }}</span>
|
<span class="text-green-400">−{{ formatSilver(result.materialCost - result.effectiveMaterialCost) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between gap-4 border-t border-gray-700/50 pt-1.5">
|
<div class="flex justify-between gap-4 border-t border-gray-700/50 pt-1.5">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { CraftingRecipe, ProfitResult, IngredientBreakdown, SortState } fro
|
|||||||
import type { FilterState, VariantType } from '../types/filters'
|
import type { FilterState, VariantType } from '../types/filters'
|
||||||
import { useAlbionPrices } from './useAlbionPrices'
|
import { useAlbionPrices } from './useAlbionPrices'
|
||||||
import { formatItemId } from '../utils/formatting'
|
import { formatItemId } from '../utils/formatting'
|
||||||
|
import { cityRrr, isRrrExempt } from '../data/cityBonuses'
|
||||||
|
|
||||||
function variantOf(outputItemId: string): VariantType {
|
function variantOf(outputItemId: string): VariantType {
|
||||||
const id = outputItemId.replace(/@\d$/, '') // strip enchantment suffix
|
const id = outputItemId.replace(/@\d$/, '') // strip enchantment suffix
|
||||||
@@ -30,7 +31,6 @@ export function useCraftingProfit(
|
|||||||
const profitResults = computed<ProfitResult[]>(() => {
|
const profitResults = computed<ProfitResult[]>(() => {
|
||||||
const f = filters.value
|
const f = filters.value
|
||||||
const city = f.city
|
const city = f.city
|
||||||
const rrrFactor = 1 - f.rrr / 100
|
|
||||||
|
|
||||||
const results: ProfitResult[] = []
|
const results: ProfitResult[] = []
|
||||||
const nameLower = f.nameFilter.trim().toLowerCase()
|
const nameLower = f.nameFilter.trim().toLowerCase()
|
||||||
@@ -44,7 +44,8 @@ export function useCraftingProfit(
|
|||||||
if (nameLower && !recipe.displayName.toLowerCase().includes(nameLower)) continue
|
if (nameLower && !recipe.displayName.toLowerCase().includes(nameLower)) continue
|
||||||
|
|
||||||
// Calculate material cost from ingredients only
|
// Calculate material cost from ingredients only
|
||||||
let materialCost = 0
|
let basicCost = 0 // resources subject to RRR
|
||||||
|
let artefactCost = 0 // artefacts / ALCHEMY_RARE — not subject to RRR
|
||||||
let missingPrices = false
|
let missingPrices = false
|
||||||
|
|
||||||
// Track oldest price date (for status column)
|
// Track oldest price date (for status column)
|
||||||
@@ -70,7 +71,8 @@ export function useCraftingProfit(
|
|||||||
} else {
|
} else {
|
||||||
trackDate(ingEntry.sell_price_min_date)
|
trackDate(ingEntry.sell_price_min_date)
|
||||||
const totalCost = unitPrice * ing.quantity
|
const totalCost = unitPrice * ing.quantity
|
||||||
materialCost += totalCost
|
if (isRrrExempt(ing.itemId)) artefactCost += totalCost
|
||||||
|
else basicCost += totalCost
|
||||||
ingredientBreakdown.push({
|
ingredientBreakdown.push({
|
||||||
itemId: ing.itemId,
|
itemId: ing.itemId,
|
||||||
displayName: formatItemId(ing.itemId),
|
displayName: formatItemId(ing.itemId),
|
||||||
@@ -81,13 +83,16 @@ export function useCraftingProfit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveMaterialCost = materialCost * rrrFactor
|
const materialCost = basicCost + artefactCost
|
||||||
|
const rrr = cityRrr(city, recipe.outputItemId)
|
||||||
|
const effectiveMaterialCost = basicCost * (1 - rrr / 100) + artefactCost
|
||||||
const priceAgeMs = missingPrices ? null : (oldestDate ? Date.now() - new Date(oldestDate).getTime() : null)
|
const priceAgeMs = missingPrices ? null : (oldestDate ? Date.now() - new Date(oldestDate).getTime() : null)
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
recipe,
|
recipe,
|
||||||
materialCost,
|
materialCost,
|
||||||
effectiveMaterialCost,
|
effectiveMaterialCost,
|
||||||
|
rrr,
|
||||||
priceAgeMs,
|
priceAgeMs,
|
||||||
missingPrices,
|
missingPrices,
|
||||||
ingredientBreakdown,
|
ingredientBreakdown,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ const STORAGE_KEY = 'albion-filters'
|
|||||||
|
|
||||||
interface StoredFilters {
|
interface StoredFilters {
|
||||||
city?: AlbionCity
|
city?: AlbionCity
|
||||||
rrr?: number
|
|
||||||
tiers?: Tier[]
|
tiers?: Tier[]
|
||||||
enchantments?: Enchantment[] | null
|
enchantments?: Enchantment[] | null
|
||||||
variants?: VariantType[] | null
|
variants?: VariantType[] | null
|
||||||
@@ -31,7 +30,6 @@ function buildInitialState(): FilterState {
|
|||||||
const s = loadStored()
|
const s = loadStored()
|
||||||
return {
|
return {
|
||||||
city: s.city ?? 'Caerleon',
|
city: s.city ?? 'Caerleon',
|
||||||
rrr: (s.rrr != null && s.rrr >= 0 && s.rrr <= 100) ? s.rrr : 15,
|
|
||||||
tiers: s.tiers ? new Set(s.tiers) : new Set(TIERS),
|
tiers: s.tiers ? new Set(s.tiers) : new Set(TIERS),
|
||||||
enchantments: s.enchantments === undefined ? null : (s.enchantments === null ? null : new Set(s.enchantments)),
|
enchantments: s.enchantments === undefined ? null : (s.enchantments === null ? null : new Set(s.enchantments)),
|
||||||
variants: s.variants === undefined ? null : (s.variants === null ? null : new Set(s.variants)),
|
variants: s.variants === undefined ? null : (s.variants === null ? null : new Set(s.variants)),
|
||||||
@@ -48,7 +46,6 @@ watch(filters, () => {
|
|||||||
const f = filters.value
|
const f = filters.value
|
||||||
const toStore: StoredFilters = {
|
const toStore: StoredFilters = {
|
||||||
city: f.city,
|
city: f.city,
|
||||||
rrr: f.rrr,
|
|
||||||
tiers: [...f.tiers],
|
tiers: [...f.tiers],
|
||||||
enchantments: f.enchantments === null ? null : [...f.enchantments],
|
enchantments: f.enchantments === null ? null : [...f.enchantments],
|
||||||
variants: f.variants === null ? null : [...f.variants],
|
variants: f.variants === null ? null : [...f.variants],
|
||||||
@@ -79,10 +76,6 @@ function setSelectedItemTypes(value: Set<string> | null) {
|
|||||||
filters.value.selectedItemTypes = value
|
filters.value.selectedItemTypes = value
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRrr(rate: number) {
|
|
||||||
filters.value.rrr = Math.max(0, Math.min(100, rate))
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNameFilter(value: string) {
|
function setNameFilter(value: string) {
|
||||||
filters.value.nameFilter = value
|
filters.value.nameFilter = value
|
||||||
}
|
}
|
||||||
@@ -123,7 +116,6 @@ export function useFilters() {
|
|||||||
setCity,
|
setCity,
|
||||||
toggleTier,
|
toggleTier,
|
||||||
setSelectedItemTypes,
|
setSelectedItemTypes,
|
||||||
setRrr,
|
|
||||||
setNameFilter,
|
setNameFilter,
|
||||||
toggleEnchantment,
|
toggleEnchantment,
|
||||||
resetEnchantments,
|
resetEnchantments,
|
||||||
|
|||||||
55
src/data/cityBonuses.ts
Normal file
55
src/data/cityBonuses.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { AlbionCity } from '../types/api'
|
||||||
|
|
||||||
|
// Source: Albion Online Wiki — Production Bonuses
|
||||||
|
// Formula: RRR% = (1 - 1 / (1 + LPB/100)) × 100
|
||||||
|
//
|
||||||
|
// Royal cities base LPB: 18% → RRR ≈ 15.3%
|
||||||
|
// Specialization bonus: +15% → total 33% LPB → RRR ≈ 24.8%
|
||||||
|
// Black Market (not a crafting location): 0% LPB → RRR = 0%
|
||||||
|
|
||||||
|
const BASE_ROYAL_LPB = 18
|
||||||
|
const SPECIALIZATION_BONUS = 15
|
||||||
|
|
||||||
|
const ROYAL_CITIES = new Set<AlbionCity>([
|
||||||
|
'Fort Sterling', 'Lymhurst', 'Bridgewatch', 'Martlock', 'Thetford', 'Caerleon',
|
||||||
|
])
|
||||||
|
|
||||||
|
// Each regex matches all output item IDs that receive the +15% specialization bonus
|
||||||
|
// in that city, including artifact / avalon / crystal variants.
|
||||||
|
const SPECIALIZATION_PATTERNS: Partial<Record<AlbionCity, RegExp>> = {
|
||||||
|
'Fort Sterling': /_MAIN_HAMMER|_2H_HAMMER|_2H_POLEHAMMER|_2H_DUALHAMMER|_2H_RAM_|_MAIN_SPEAR|_2H_SPEAR|_2H_GLAIVE|_2H_HARPOON|_2H_TRIDENT|_HOLYSTAFF|_DIVINESTAFF|_HEAD_PLATE_|_ARMOR_CLOTH_/,
|
||||||
|
'Lymhurst': /_MAIN_SWORD|_2H_CLAYMORE|_2H_DUALSWORD|_SCIMITAR|_2H_CLEAVER|_2H_BOW|_LONGBOW|_WARBOW|_ARCANESTAFF|_ENIGMATICSTAFF|_ENIGMATICORB|_ARCANE_RINGPAIR|_HEAD_LEATHER_|_SHOES_LEATHER_/,
|
||||||
|
'Bridgewatch': /_CROSSBOW|_1HCROSSBOW|_DAGGER|_CLAWPAIR|_RAPIER|_DUALSICKLE|_CURSEDSTAFF|_DEMONICSTAFF|_SKULLORB|_ARMOR_PLATE_|_SHOES_CLOTH_/,
|
||||||
|
'Martlock': /_MAIN_AXE|_2H_AXE|_2H_HALBERD|_2H_SCYTHE|_2H_DUALAXE|_QUARTERSTAFF|_IRONCLADEDSTAFF|_DOUBLEBLADEDSTAFF|_COMBATSTAFF|_TWINSCYTHE|_ROCKSTAFF|_FROSTSTAFF|_GLACIALSTAFF|_ICEGAUNTLETS|_ICECRYSTAL|_SHOES_PLATE_|_OFF_/,
|
||||||
|
'Thetford': /_MAIN_MACE|_2H_MACE|_2H_FLAIL|_ROCKMACE|_2H_DUALMACE|_NATURESTAFF|_WILDSTAFF|_FIRESTAFF|_INFERNOSTAFF|_FIRE_RINGPAIR|_ARMOR_LEATHER_|_HEAD_CLOTH_/,
|
||||||
|
'Caerleon': /_2H_KNUCKLES|_SHAPESHIFTER|_2H_TOOL_/,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the Local Production Bonus (%) for crafting this item in the given city.
|
||||||
|
export function localProductionBonus(city: AlbionCity, outputItemId: string): number {
|
||||||
|
if (!ROYAL_CITIES.has(city)) return 0 // Black Market: no bonus
|
||||||
|
|
||||||
|
const pattern = SPECIALIZATION_PATTERNS[city]
|
||||||
|
const baseId = outputItemId.replace(/@\d$/, '') // strip enchantment suffix
|
||||||
|
const isSpecialized = pattern ? pattern.test(baseId) : false
|
||||||
|
|
||||||
|
return BASE_ROYAL_LPB + (isSpecialized ? SPECIALIZATION_BONUS : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts a Local Production Bonus (%) to the resulting Resource Return Rate (%).
|
||||||
|
// Formula: RRR = 1 - 1/(1 + LPB/100)
|
||||||
|
export function rrrFromBonus(lpb: number): number {
|
||||||
|
if (lpb === 0) return 0
|
||||||
|
return (1 - 1 / (1 + lpb / 100)) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the effective RRR (%) for crafting this item in the given city.
|
||||||
|
export function cityRrr(city: AlbionCity, outputItemId: string): number {
|
||||||
|
return rrrFromBonus(localProductionBonus(city, outputItemId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true for ingredients that are NOT subject to RRR (artefacts, ALCHEMY_RARE).
|
||||||
|
// Only basic resources (bars, planks, leather, cloth) are returned by the city bonus.
|
||||||
|
export function isRrrExempt(itemId: string): boolean {
|
||||||
|
return itemId.includes('_ARTEFACT_') || itemId.includes('_ALCHEMY_RARE_')
|
||||||
|
}
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
<span class="text-gray-200">{{ formatSilver(totals.rawCost) }}</span>
|
<span class="text-gray-200">{{ formatSilver(totals.rawCost) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between gap-4">
|
<div class="flex justify-between gap-4">
|
||||||
<span class="text-gray-400">RRR savings ({{ filters.rrr }}%)</span>
|
<span class="text-gray-400">RRR savings</span>
|
||||||
<span class="text-green-400">−{{ formatSilver(totals.rrrSavings) }}</span>
|
<span class="text-green-400">−{{ formatSilver(totals.rrrSavings) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between gap-4 border-t border-gray-700/50 pt-2">
|
<div class="flex justify-between gap-4 border-t border-gray-700/50 pt-2">
|
||||||
@@ -209,6 +209,7 @@ import { useAlbionPrices } from '../composables/useAlbionPrices'
|
|||||||
import { useFilters } from '../composables/useFilters'
|
import { useFilters } from '../composables/useFilters'
|
||||||
import { formatSilver, formatItemId, formatLastUpdated, tierStyle, itemImageUrl } from '../utils/formatting'
|
import { formatSilver, formatItemId, formatLastUpdated, tierStyle, itemImageUrl } from '../utils/formatting'
|
||||||
import { fameTypeOf, craftsPerJournal } from '../data/constants'
|
import { fameTypeOf, craftsPerJournal } from '../data/constants'
|
||||||
|
import { cityRrr, isRrrExempt } from '../data/cityBonuses'
|
||||||
import type { Tier, JournalType } from '../types/crafting'
|
import type { Tier, JournalType } from '../types/crafting'
|
||||||
|
|
||||||
const vFocus = { mounted: (el: HTMLElement) => el.focus() }
|
const vFocus = { mounted: (el: HTMLElement) => el.focus() }
|
||||||
@@ -277,11 +278,11 @@ const JOURNAL_ITEM_ID: Record<JournalType, string> = {
|
|||||||
// ─── Per-item computed values ─────────────────────────────────────────────────
|
// ─── Per-item computed values ─────────────────────────────────────────────────
|
||||||
|
|
||||||
const computedItems = computed(() => {
|
const computedItems = computed(() => {
|
||||||
const city = filters.value.city
|
const city = filters.value.city
|
||||||
const rrrFactor = 1 - filters.value.rrr / 100
|
|
||||||
|
|
||||||
return orderItems.value.map(({ recipe, qty }) => {
|
return orderItems.value.map(({ recipe, qty }) => {
|
||||||
let rawCost = 0
|
let basicCost = 0
|
||||||
|
let artefactCost = 0
|
||||||
let missingPrices = false
|
let missingPrices = false
|
||||||
|
|
||||||
for (const ing of recipe.ingredients) {
|
for (const ing of recipe.ingredients) {
|
||||||
@@ -289,12 +290,15 @@ const computedItems = computed(() => {
|
|||||||
if (!entry || entry.sell_price_min === 0) {
|
if (!entry || entry.sell_price_min === 0) {
|
||||||
missingPrices = true
|
missingPrices = true
|
||||||
} else {
|
} else {
|
||||||
rawCost += entry.sell_price_min * ing.quantity
|
if (isRrrExempt(ing.itemId)) artefactCost += entry.sell_price_min * ing.quantity
|
||||||
|
else basicCost += entry.sell_price_min * ing.quantity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const craftCostPerUnit = rawCost * rrrFactor
|
const rawCost = basicCost + artefactCost
|
||||||
return { recipe, qty, craftCostPerUnit, totalCraftCost: craftCostPerUnit * qty, missingPrices }
|
const rrr = cityRrr(city, recipe.outputItemId)
|
||||||
|
const craftCostPerUnit = basicCost * (1 - rrr / 100) + artefactCost
|
||||||
|
return { recipe, qty, rawCostPerUnit: rawCost, craftCostPerUnit, totalCraftCost: craftCostPerUnit * qty, rrr, missingPrices }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -323,21 +327,21 @@ const aggregatedMaterials = computed(() => {
|
|||||||
// ─── Totals + journals ────────────────────────────────────────────────────────
|
// ─── Totals + journals ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const totals = computed(() => {
|
const totals = computed(() => {
|
||||||
const rrrFactor = 1 - filters.value.rrr / 100
|
|
||||||
let rawCost = 0
|
let rawCost = 0
|
||||||
|
let effectiveCost = 0
|
||||||
let totalCrafts = 0
|
let totalCrafts = 0
|
||||||
let totalItems = 0
|
let totalItems = 0
|
||||||
|
|
||||||
for (const item of computedItems.value) {
|
for (const item of computedItems.value) {
|
||||||
if (!item.missingPrices) {
|
if (!item.missingPrices) {
|
||||||
rawCost += item.craftCostPerUnit / rrrFactor * item.qty // back to raw
|
rawCost += item.rawCostPerUnit * item.qty
|
||||||
|
effectiveCost += item.craftCostPerUnit * item.qty
|
||||||
}
|
}
|
||||||
totalCrafts += item.qty
|
totalCrafts += item.qty
|
||||||
totalItems += item.qty
|
totalItems += item.qty
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveCost = rawCost * rrrFactor
|
const rrrSavings = rawCost - effectiveCost
|
||||||
const rrrSavings = rawCost - effectiveCost
|
|
||||||
|
|
||||||
return { rawCost, effectiveCost, rrrSavings, totalCrafts, totalItems }
|
return { rawCost, effectiveCost, rrrSavings, totalCrafts, totalItems }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface ProfitResult {
|
|||||||
recipe: CraftingRecipe
|
recipe: CraftingRecipe
|
||||||
materialCost: number
|
materialCost: number
|
||||||
effectiveMaterialCost: number
|
effectiveMaterialCost: number
|
||||||
|
rrr: number // effective RRR % for this item in the selected city
|
||||||
priceAgeMs: number | null // null = missing prices; ms since oldest manual entry
|
priceAgeMs: number | null // null = missing prices; ms since oldest manual entry
|
||||||
missingPrices: boolean
|
missingPrices: boolean
|
||||||
ingredientBreakdown: IngredientBreakdown[]
|
ingredientBreakdown: IngredientBreakdown[]
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export interface FilterState {
|
|||||||
city: AlbionCity
|
city: AlbionCity
|
||||||
tiers: Set<Tier>
|
tiers: Set<Tier>
|
||||||
selectedItemTypes: Set<string> | null
|
selectedItemTypes: Set<string> | null
|
||||||
rrr: number
|
|
||||||
nameFilter: string
|
nameFilter: string
|
||||||
enchantments: Set<Enchantment> | null
|
enchantments: Set<Enchantment> | null
|
||||||
variants: Set<VariantType> | null
|
variants: Set<VariantType> | null
|
||||||
|
|||||||
Reference in New Issue
Block a user