From 642e47be77377c823e67ad1f262fa5d7c9f54a10 Mon Sep 17 00:00:00 2001 From: theit8514 Date: Sun, 7 Jun 2026 11:02:23 -0400 Subject: [PATCH 1/4] feat(burn): add solver for ship size --- .../xit/components/XITBurnActionButton.vue | 126 ++++- src/features/xit/useBurnSolver.ts | 500 ++++++++++++++++++ src/features/xit/useBurnXITAction.ts | 55 +- src/features/xit/xitAction.types.ts | 12 + src/locales/de_DE/xit.json | 7 + src/locales/en_US/xit.json | 7 + src/locales/es_ES/xit.json | 7 + src/locales/fr_FR/xit.json | 7 + src/locales/it_IT/xit.json | 7 + src/locales/ja_JP/xit.json | 7 + src/locales/ko_KR/xit.json | 7 + src/locales/nl_NL/xit.json | 7 + src/locales/pt_PT/xit.json | 7 + src/locales/ru_RU/xit.json | 7 + src/locales/zh_CN/xit.json | 7 + src/tests/features/xit/useBurnSolver.test.ts | 327 ++++++++++++ .../features/xit/useXITBurnAction.test.ts | 70 ++- 17 files changed, 1140 insertions(+), 27 deletions(-) create mode 100644 src/features/xit/useBurnSolver.ts create mode 100644 src/tests/features/xit/useBurnSolver.test.ts diff --git a/src/features/xit/components/XITBurnActionButton.vue b/src/features/xit/components/XITBurnActionButton.vue index e641781e..ccd84468 100644 --- a/src/features/xit/components/XITBurnActionButton.vue +++ b/src/features/xit/components/XITBurnActionButton.vue @@ -36,8 +36,10 @@ PSelect, PInput, PTable, + PTooltip, } from "@/ui"; import { NDrawer, NDrawerContent } from "naive-ui"; + import { HelpOutlineSharp } from "@vicons/material"; // Constants import { XITSTATIONWAREHOUSES } from "@/features/xit/xitConstants"; @@ -104,17 +106,36 @@ const refHideInfinite: Ref = ref(false); const refMaterialOverrides: Ref> = ref({}); const refMaterialInactives: Ref> = ref(new Set([])); + const refBurnMode: Ref<"simple" | "solver"> = ref("simple"); + const refShipWeightCapacity: Ref = ref(1000); + const refShipVolumeCapacity: Ref = ref(1000); + const refFullCoverThreshold: Ref = ref(1.0); + + const modeOptions = computed(() => [ + { label: t("xit.form.mode_simple"), value: "simple" }, + { label: t("xit.form.mode_solver"), value: "solver" }, + ]); const { materialTable, totalWeightVolume, totalPrice, fit } = await useBurnXITAction( + refBurnMode, localElements, burnResupplyDays, refHideInfinite, refMaterialOverrides, refMaterialInactives, ref(undefined), - ref(undefined) + ref(undefined), + refShipWeightCapacity, + refShipVolumeCapacity, + refFullCoverThreshold ); + + function applyShipPreset(weight: number, volume: number): void { + refShipWeightCapacity.value = weight; + refShipVolumeCapacity.value = volume; + trackEvent("xit_burn_fit_ship", { weight, volume }); + } + diff --git a/src/features/xit/useBurnSolver.ts b/src/features/xit/useBurnSolver.ts new file mode 100644 index 00000000..341dfb47 --- /dev/null +++ b/src/features/xit/useBurnSolver.ts @@ -0,0 +1,500 @@ +import { + solve, + lessEq, + greaterEq, + Model, + Constraint, + Solution, +} from "yalps"; + +import { IMaterial } from "@/features/api/gameData.types"; +import { + IBurnDemandItem, + IXITActionElement, +} from "@/features/xit/xitAction.types"; + +const EPSILON = 1e-6; + +export interface IBurnPrefillResult { + selected: Map; + usedVolume: number; + usedWeight: number; + guaranteed: Set; +} + +export interface IBurnSolveOptions { + volumeCapacity: number; + weightCapacity: number; + targetDays: number; + fullCoverBelowBurnPerDay?: number; + integer?: boolean; + allowOverTarget?: boolean; +} + +function varName(ticker: string): string { + return "x_" + ticker; +} + +function loadCap( + item: IBurnDemandItem, + allowOverTarget: boolean, + volumeCapacity: number, + weightCapacity: number +): number { + if (!allowOverTarget) { + return item.need; + } + const byVolume = + item.volumePerUnit > 0 + ? volumeCapacity / item.volumePerUnit + : Infinity; + const byWeight = + item.weightPerUnit > 0 + ? weightCapacity / item.weightPerUnit + : Infinity; + return Math.max(item.need, Math.floor(Math.min(byVolume, byWeight))); +} + +function readSolution( + result: Solution, + items: IBurnDemandItem[] +): Map { + const selected = new Map(); + const varMap = new Map(result.variables); + for (const item of items) { + const raw = varMap.get(varName(item.ticker)) ?? 0; + selected.set(item.ticker, raw); + } + return selected; +} + +function minDays( + selected: Map, + items: IBurnDemandItem[] +): number { + let min = Infinity; + for (const item of items) { + if (item.burnPerDay <= 0) continue; + const qty = selected.get(item.ticker) ?? 0; + min = Math.min( + min, + (item.currentStock + qty) / item.burnPerDay + ); + } + return Number.isFinite(min) ? min : 0; +} + +export function buildBurnDemand( + elements: IXITActionElement[], + materialsMap: Record, + targetDays: number, + defaultPriority = 1 +): IBurnDemandItem[] { + const items: IBurnDemandItem[] = []; + + for (const element of elements) { + if (element.delta >= 0) continue; + + const material = materialsMap[element.ticker]; + if (!material) continue; + + const weightPerUnit = material.weight; + const volumePerUnit = material.volume; + if ( + !Number.isFinite(weightPerUnit) || + weightPerUnit <= 0 || + !Number.isFinite(volumePerUnit) || + volumePerUnit <= 0 + ) { + continue; + } + + const burnPerDay = element.delta * -1; + const targetQuantity = burnPerDay * targetDays; + const currentStock = element.stock; + const need = Math.max(0, targetQuantity - currentStock); + + items.push({ + ticker: element.ticker, + burnPerDay, + targetDays, + targetQuantity, + currentStock, + need, + weightPerUnit, + volumePerUnit, + priority: defaultPriority, + }); + } + + return items; +} + +export function prefillGuaranteed( + items: IBurnDemandItem[], + { + volumeCapacity, + weightCapacity, + integer, + thresholdBurn, + }: { + volumeCapacity: number; + weightCapacity: number; + integer: boolean; + thresholdBurn: number; + } +): IBurnPrefillResult { + const selected = new Map(); + const guaranteed = new Set(); + let usedVolume = 0; + let usedWeight = 0; + + if (!(thresholdBurn > 0)) { + return { selected, usedVolume, usedWeight, guaranteed }; + } + + const candidates = items + .filter( + (i) => i.burnPerDay > 0 && i.burnPerDay <= thresholdBurn + ) + .sort( + (a, b) => + a.burnPerDay - b.burnPerDay || b.priority - a.priority + ); + + for (const item of candidates) { + guaranteed.add(item.ticker); + const want = integer ? Math.ceil(item.need) : item.need; + if (!(want > 0)) { + selected.set(item.ticker, 0); + continue; + } + const volLeft = volumeCapacity - usedVolume; + const weightLeft = weightCapacity - usedWeight; + const byVolume = + item.volumePerUnit > 0 + ? volLeft / item.volumePerUnit + : Infinity; + const byWeight = + item.weightPerUnit > 0 + ? weightLeft / item.weightPerUnit + : Infinity; + let qty = Math.min(want, byVolume, byWeight); + if (integer) { + qty = Math.floor(qty); + } + qty = Math.max(0, qty); + selected.set(item.ticker, qty); + usedVolume += qty * item.volumePerUnit; + usedWeight += qty * item.weightPerUnit; + } + + return { selected, usedVolume, usedWeight, guaranteed }; +} + +function solveMaxMinDays({ + items, + volumeCapacity, + weightCapacity, + allowOverTarget, +}: { + items: IBurnDemandItem[]; + volumeCapacity: number; + weightCapacity: number; + allowOverTarget: boolean; +}): number { + const eligible = items.filter( + (i) => + loadCap(i, allowOverTarget, volumeCapacity, weightCapacity) > + 0 && i.burnPerDay > 0 + ); + if (eligible.length === 0) { + return 0; + } + + const maxTargetDays = Math.max(...eligible.map((i) => i.targetDays)); + const constraints = new Map(); + constraints.set("zmax", lessEq(maxTargetDays)); + constraints.set("vol", lessEq(volumeCapacity)); + constraints.set("weight", lessEq(weightCapacity)); + + const zVariable = new Map([ + ["obj", 1], + ["zmax", 1], + ]); + const variables = new Map>([ + ["z", zVariable], + ]); + + for (const item of items) { + const v = varName(item.ticker); + const cap = loadCap( + item, + allowOverTarget, + volumeCapacity, + weightCapacity + ); + const capConstraintName = "cap_" + item.ticker; + const variable = new Map([ + ["vol", item.volumePerUnit], + ["weight", item.weightPerUnit], + [capConstraintName, 1], + ]); + constraints.set(capConstraintName, lessEq(cap)); + + if (eligible.includes(item)) { + const daysConstraintName = "days_" + item.ticker; + variable.set(daysConstraintName, 1 / item.burnPerDay); + zVariable.set(daysConstraintName, -1); + constraints.set( + daysConstraintName, + greaterEq(-item.currentStock / item.burnPerDay) + ); + } + variables.set(v, variable); + } + + const model: Model = { + direction: "maximize", + objective: "obj", + constraints, + variables, + integers: [], + }; + + const result = solve(model, { includeZeroVariables: true }); + if (result.status !== "optimal") { + return 0; + } + + const varMap = new Map(result.variables); + return Math.max(0, varMap.get("z") ?? 0); +} + +function solveWeighted({ + items, + volumeCapacity, + weightCapacity, + integer, + allowOverTarget, + floors, +}: { + items: IBurnDemandItem[]; + volumeCapacity: number; + weightCapacity: number; + integer: boolean; + allowOverTarget: boolean; + floors?: Map; +}): Map | null { + const constraints = new Map(); + constraints.set("vol", lessEq(volumeCapacity)); + constraints.set("weight", lessEq(weightCapacity)); + + const variables = new Map>(); + const integers: string[] = []; + + for (const item of items) { + const v = varName(item.ticker); + const cap = loadCap( + item, + allowOverTarget, + volumeCapacity, + weightCapacity + ); + const capConstraintName = "cap_" + item.ticker; + const variable = new Map([ + ["obj", item.priority / item.targetQuantity], + ["vol", item.volumePerUnit], + ["weight", item.weightPerUnit], + [capConstraintName, 1], + ]); + constraints.set(capConstraintName, lessEq(cap)); + + const floor = floors?.get(item.ticker) ?? 0; + if (floor > 0) { + const floorConstraintName = "floor_" + item.ticker; + variable.set(floorConstraintName, 1); + constraints.set( + floorConstraintName, + greaterEq(Math.max(0, floor - EPSILON)) + ); + } + variables.set(v, variable); + if (integer) { + integers.push(v); + } + } + + const model: Model = { + direction: "maximize", + objective: "obj", + constraints, + variables, + integers, + }; + + const result = solve(model, { includeZeroVariables: true }); + if (result.status !== "optimal") { + return null; + } + return readSolution(result, items); +} + +function solveGreedy( + items: IBurnDemandItem[], + volumeCapacity: number, + weightCapacity: number, + integer: boolean, + allowOverTarget: boolean, + thresholdBurn: number +): Map { + const prefill = prefillGuaranteed(items, { + volumeCapacity, + weightCapacity, + integer, + thresholdBurn, + }); + + let volumeLeft = volumeCapacity - prefill.usedVolume; + let weightLeft = weightCapacity - prefill.usedWeight; + + const remaining = items.filter( + (i) => !prefill.guaranteed.has(i.ticker) + ); + const maxBurn = Math.max(1, ...remaining.map((i) => i.burnPerDay)); + const scored = remaining + .map((item) => ({ + item, + score: item.priority + 0.5 * (item.burnPerDay / maxBurn), + })) + .sort((a, b) => b.score - a.score); + + const selected = new Map(prefill.selected); + for (const { item } of scored) { + const byVolume0 = + item.volumePerUnit > 0 + ? volumeLeft / item.volumePerUnit + : Infinity; + const byWeight0 = + item.weightPerUnit > 0 + ? weightLeft / item.weightPerUnit + : Infinity; + const cap = allowOverTarget + ? Math.max( + item.need, + Math.floor(Math.min(byVolume0, byWeight0)) + ) + : item.need; + if (cap <= 0) { + selected.set(item.ticker, 0); + continue; + } + const byVolume = + item.volumePerUnit > 0 + ? volumeLeft / item.volumePerUnit + : Infinity; + const byWeight = + item.weightPerUnit > 0 + ? weightLeft / item.weightPerUnit + : Infinity; + let qty = Math.min(cap, byVolume, byWeight); + if (integer) { + qty = Math.floor(qty); + } + qty = Math.max(0, qty); + selected.set(item.ticker, qty); + volumeLeft -= qty * item.volumePerUnit; + weightLeft -= qty * item.weightPerUnit; + } + + return selected; +} + +export function solveBurn( + items: IBurnDemandItem[], + options: IBurnSolveOptions +): Map { + const integer = options.integer ?? true; + const allowOverTarget = options.allowOverTarget ?? false; + const thresholdBurn = options.fullCoverBelowBurnPerDay ?? 1.0; + + if (items.length === 0) { + return new Map(); + } + + const prefill = prefillGuaranteed(items, { + volumeCapacity: options.volumeCapacity, + weightCapacity: options.weightCapacity, + integer, + thresholdBurn, + }); + + const remaining = items.filter( + (i) => !prefill.guaranteed.has(i.ticker) + ); + const volumeCapacity = Math.max( + 0, + options.volumeCapacity - prefill.usedVolume + ); + const weightCapacity = Math.max( + 0, + options.weightCapacity - prefill.usedWeight + ); + + const finalize = (selected: Map): Map => { + const merged = new Map(prefill.selected); + for (const [ticker, qty] of selected) { + merged.set(ticker, qty); + } + return merged; + }; + + if (remaining.length === 0) { + return finalize(new Map()); + } + + const base = { + items: remaining, + volumeCapacity, + weightCapacity, + integer, + allowOverTarget, + }; + + const zDays = solveMaxMinDays(base); + const floors = new Map(); + for (const item of remaining) { + if ( + loadCap(item, allowOverTarget, volumeCapacity, weightCapacity) > + 0 + ) { + const floorQty = Math.max( + 0, + zDays * item.burnPerDay - item.currentStock + ); + floors.set( + item.ticker, + integer ? Math.floor(floorQty) : floorQty + ); + } + } + + let stage2 = solveWeighted({ ...base, floors }); + if (!stage2) { + stage2 = solveWeighted({ ...base }); + } + if (!stage2) { + return solveGreedy( + items, + options.volumeCapacity, + options.weightCapacity, + integer, + allowOverTarget, + thresholdBurn + ); + } + + return finalize(stage2); +} + +export { minDays }; diff --git a/src/features/xit/useBurnXITAction.ts b/src/features/xit/useBurnXITAction.ts index 2bd2fdcb..f4fed55a 100644 --- a/src/features/xit/useBurnXITAction.ts +++ b/src/features/xit/useBurnXITAction.ts @@ -3,6 +3,7 @@ import { computed, ComputedRef, Ref, ref, watchEffect } from "vue"; // Composables import { useMaterialData } from "@/database/services/useMaterialData"; import { usePrice } from "@/features/cx/usePrice"; +import { buildBurnDemand, solveBurn } from "@/features/xit/useBurnSolver"; // Types & Interfaces import { @@ -12,13 +13,17 @@ import { import { IMaterial } from "@/features/api/gameData.types"; export async function useBurnXITAction( + burnMode: Ref<"simple" | "solver"> = ref("simple"), elements: Ref, resupplyDays: Ref, hideInfinite: Ref, materialOverrides: Ref>, materialInactives: Ref>, cxUuid: Ref, - planetNaturalId: Ref + planetNaturalId: Ref, + shipWeightCapacity: Ref = ref(1000), + shipVolumeCapacity: Ref = ref(1000), + fullCoverThreshold: Ref = ref(1.0) ) { // get materials const { materialsMap } = useMaterialData(); @@ -26,6 +31,10 @@ export async function useBurnXITAction( // get price function const { getPrice } = await usePrice(cxUuid, planetNaturalId); + const solverMode: ComputedRef = computed( + () => burnMode.value === "solver" + ); + // buildupo material overrides materialOverrides.value = elements.value.reduce( (sum, current) => { @@ -46,6 +55,29 @@ export async function useBurnXITAction( const materialTable: ComputedRef = computed( () => { const tableElements: IXITActionMaterialElement[] = []; + let solverQuantities = new Map(); + + if (solverMode.value) { + const activeElements = elements.value.filter((e) => { + const override = materialOverrides.value[e.ticker]; + return ( + !materialInactives.value.has(e.ticker) && + (override == null || override <= 0) + ); + }); + const demand = buildBurnDemand( + activeElements, + materialsMap.value, + resupplyDays.value + ); + solverQuantities = solveBurn(demand, { + volumeCapacity: shipVolumeCapacity.value, + weightCapacity: shipWeightCapacity.value, + targetDays: resupplyDays.value, + fullCoverBelowBurnPerDay: fullCoverThreshold.value, + integer: true, + }); + } elements.value.forEach((e) => { let totalNeed: number = Infinity; @@ -62,17 +94,28 @@ export async function useBurnXITAction( const override: number | undefined = value != null && value > 0 ? value : undefined; + let total: number; + if (override) { + total = override; + } else if (solverMode.value && e.delta < 0) { + total = Math.max( + 0, + Math.round(solverQuantities.get(e.ticker) ?? 0) + ); + } else { + total = + Math.ceil(totalNeed) > 0 + ? Math.ceil(totalNeed) + : 0; + } + tableElements.push({ active: !materialInactives.value.has(e.ticker), ticker: e.ticker, stock: e.stock, delta: e.delta, burn: e.delta > 0 ? Infinity : e.stock / (e.delta * -1), - total: override - ? override - : Math.ceil(totalNeed) > 0 - ? Math.ceil(totalNeed) - : 0, + total, }); } }); diff --git a/src/features/xit/xitAction.types.ts b/src/features/xit/xitAction.types.ts index 4c3c6bb9..98749793 100644 --- a/src/features/xit/xitAction.types.ts +++ b/src/features/xit/xitAction.types.ts @@ -53,3 +53,15 @@ export interface IXITTransferMaterial { ticker: string; value: number; } + +export interface IBurnDemandItem { + ticker: string; + burnPerDay: number; + targetDays: number; + targetQuantity: number; + currentStock: number; + need: number; + weightPerUnit: number; + volumePerUnit: number; + priority: number; +} diff --git a/src/locales/de_DE/xit.json b/src/locales/de_DE/xit.json index 53001e25..a249c280 100644 --- a/src/locales/de_DE/xit.json +++ b/src/locales/de_DE/xit.json @@ -1,10 +1,17 @@ { "form": { + "mode": "Mode", + "mode_simple": "Simple (days)", + "mode_solver": "Solver (ship target)", "origin": "Herkunft", "target_days": "Ziel-Tage", "buy_from_cx": "Von CX Kaufen", "buy_from_cx_warning": "Nur Warenhäuser als Ursprung erlauben den direkten Einkauf.", "fit_ship": "Auf Schiff anpassen", + "ship_weight": "Ship Weight (t)", + "ship_volume": "Ship Volume (m³)", + "full_cover_threshold": "Full Cover", + "full_cover_threshold_info": "Materials whose daily burn is at or below this value are shipped to their full target-days quantity first, before the solver balances the rest of the cargo. This guarantees tiny-burn items are not starved by the optimization. Set to 0 to disable.", "hide_infinite": "Unendliche ausblenden", "json": "JSON" }, diff --git a/src/locales/en_US/xit.json b/src/locales/en_US/xit.json index 7d505bd8..4f68efc9 100644 --- a/src/locales/en_US/xit.json +++ b/src/locales/en_US/xit.json @@ -1,10 +1,17 @@ { "form": { + "mode": "Mode", + "mode_simple": "Simple (days)", + "mode_solver": "Solver (ship target)", "origin": "Origin", "target_days": "Target Days", "buy_from_cx": "Buy From CX", "buy_from_cx_warning": "Only warehouse origin allows purchasing.", "fit_ship": "Fit Ship", + "ship_weight": "Ship Weight (t)", + "ship_volume": "Ship Volume (m³)", + "full_cover_threshold": "Full Cover", + "full_cover_threshold_info": "Materials whose daily burn is at or below this value are shipped to their full target-days quantity first, before the solver balances the rest of the cargo. This guarantees tiny-burn items are not starved by the optimization. Set to 0 to disable.", "hide_infinite": "Hide Infinite", "json": "JSON" }, diff --git a/src/locales/es_ES/xit.json b/src/locales/es_ES/xit.json index 7d505bd8..4f68efc9 100644 --- a/src/locales/es_ES/xit.json +++ b/src/locales/es_ES/xit.json @@ -1,10 +1,17 @@ { "form": { + "mode": "Mode", + "mode_simple": "Simple (days)", + "mode_solver": "Solver (ship target)", "origin": "Origin", "target_days": "Target Days", "buy_from_cx": "Buy From CX", "buy_from_cx_warning": "Only warehouse origin allows purchasing.", "fit_ship": "Fit Ship", + "ship_weight": "Ship Weight (t)", + "ship_volume": "Ship Volume (m³)", + "full_cover_threshold": "Full Cover", + "full_cover_threshold_info": "Materials whose daily burn is at or below this value are shipped to their full target-days quantity first, before the solver balances the rest of the cargo. This guarantees tiny-burn items are not starved by the optimization. Set to 0 to disable.", "hide_infinite": "Hide Infinite", "json": "JSON" }, diff --git a/src/locales/fr_FR/xit.json b/src/locales/fr_FR/xit.json index 477b90a5..c01590b8 100644 --- a/src/locales/fr_FR/xit.json +++ b/src/locales/fr_FR/xit.json @@ -1,10 +1,17 @@ { "form": { + "mode": "Mode", + "mode_simple": "Simple (days)", + "mode_solver": "Solver (ship target)", "origin": "Origine", "target_days": "Target Days", "buy_from_cx": "Buy From CX", "buy_from_cx_warning": "Only warehouse origin allows purchasing.", "fit_ship": "Fit Ship", + "ship_weight": "Ship Weight (t)", + "ship_volume": "Ship Volume (m³)", + "full_cover_threshold": "Full Cover", + "full_cover_threshold_info": "Materials whose daily burn is at or below this value are shipped to their full target-days quantity first, before the solver balances the rest of the cargo. This guarantees tiny-burn items are not starved by the optimization. Set to 0 to disable.", "hide_infinite": "Hide Infinite", "json": "JSON" }, diff --git a/src/locales/it_IT/xit.json b/src/locales/it_IT/xit.json index 7d505bd8..4f68efc9 100644 --- a/src/locales/it_IT/xit.json +++ b/src/locales/it_IT/xit.json @@ -1,10 +1,17 @@ { "form": { + "mode": "Mode", + "mode_simple": "Simple (days)", + "mode_solver": "Solver (ship target)", "origin": "Origin", "target_days": "Target Days", "buy_from_cx": "Buy From CX", "buy_from_cx_warning": "Only warehouse origin allows purchasing.", "fit_ship": "Fit Ship", + "ship_weight": "Ship Weight (t)", + "ship_volume": "Ship Volume (m³)", + "full_cover_threshold": "Full Cover", + "full_cover_threshold_info": "Materials whose daily burn is at or below this value are shipped to their full target-days quantity first, before the solver balances the rest of the cargo. This guarantees tiny-burn items are not starved by the optimization. Set to 0 to disable.", "hide_infinite": "Hide Infinite", "json": "JSON" }, diff --git a/src/locales/ja_JP/xit.json b/src/locales/ja_JP/xit.json index 7d505bd8..4f68efc9 100644 --- a/src/locales/ja_JP/xit.json +++ b/src/locales/ja_JP/xit.json @@ -1,10 +1,17 @@ { "form": { + "mode": "Mode", + "mode_simple": "Simple (days)", + "mode_solver": "Solver (ship target)", "origin": "Origin", "target_days": "Target Days", "buy_from_cx": "Buy From CX", "buy_from_cx_warning": "Only warehouse origin allows purchasing.", "fit_ship": "Fit Ship", + "ship_weight": "Ship Weight (t)", + "ship_volume": "Ship Volume (m³)", + "full_cover_threshold": "Full Cover", + "full_cover_threshold_info": "Materials whose daily burn is at or below this value are shipped to their full target-days quantity first, before the solver balances the rest of the cargo. This guarantees tiny-burn items are not starved by the optimization. Set to 0 to disable.", "hide_infinite": "Hide Infinite", "json": "JSON" }, diff --git a/src/locales/ko_KR/xit.json b/src/locales/ko_KR/xit.json index 7d505bd8..4f68efc9 100644 --- a/src/locales/ko_KR/xit.json +++ b/src/locales/ko_KR/xit.json @@ -1,10 +1,17 @@ { "form": { + "mode": "Mode", + "mode_simple": "Simple (days)", + "mode_solver": "Solver (ship target)", "origin": "Origin", "target_days": "Target Days", "buy_from_cx": "Buy From CX", "buy_from_cx_warning": "Only warehouse origin allows purchasing.", "fit_ship": "Fit Ship", + "ship_weight": "Ship Weight (t)", + "ship_volume": "Ship Volume (m³)", + "full_cover_threshold": "Full Cover", + "full_cover_threshold_info": "Materials whose daily burn is at or below this value are shipped to their full target-days quantity first, before the solver balances the rest of the cargo. This guarantees tiny-burn items are not starved by the optimization. Set to 0 to disable.", "hide_infinite": "Hide Infinite", "json": "JSON" }, diff --git a/src/locales/nl_NL/xit.json b/src/locales/nl_NL/xit.json index 7d505bd8..4f68efc9 100644 --- a/src/locales/nl_NL/xit.json +++ b/src/locales/nl_NL/xit.json @@ -1,10 +1,17 @@ { "form": { + "mode": "Mode", + "mode_simple": "Simple (days)", + "mode_solver": "Solver (ship target)", "origin": "Origin", "target_days": "Target Days", "buy_from_cx": "Buy From CX", "buy_from_cx_warning": "Only warehouse origin allows purchasing.", "fit_ship": "Fit Ship", + "ship_weight": "Ship Weight (t)", + "ship_volume": "Ship Volume (m³)", + "full_cover_threshold": "Full Cover", + "full_cover_threshold_info": "Materials whose daily burn is at or below this value are shipped to their full target-days quantity first, before the solver balances the rest of the cargo. This guarantees tiny-burn items are not starved by the optimization. Set to 0 to disable.", "hide_infinite": "Hide Infinite", "json": "JSON" }, diff --git a/src/locales/pt_PT/xit.json b/src/locales/pt_PT/xit.json index 7d505bd8..4f68efc9 100644 --- a/src/locales/pt_PT/xit.json +++ b/src/locales/pt_PT/xit.json @@ -1,10 +1,17 @@ { "form": { + "mode": "Mode", + "mode_simple": "Simple (days)", + "mode_solver": "Solver (ship target)", "origin": "Origin", "target_days": "Target Days", "buy_from_cx": "Buy From CX", "buy_from_cx_warning": "Only warehouse origin allows purchasing.", "fit_ship": "Fit Ship", + "ship_weight": "Ship Weight (t)", + "ship_volume": "Ship Volume (m³)", + "full_cover_threshold": "Full Cover", + "full_cover_threshold_info": "Materials whose daily burn is at or below this value are shipped to their full target-days quantity first, before the solver balances the rest of the cargo. This guarantees tiny-burn items are not starved by the optimization. Set to 0 to disable.", "hide_infinite": "Hide Infinite", "json": "JSON" }, diff --git a/src/locales/ru_RU/xit.json b/src/locales/ru_RU/xit.json index b5674ba5..8b155bbc 100644 --- a/src/locales/ru_RU/xit.json +++ b/src/locales/ru_RU/xit.json @@ -1,10 +1,17 @@ { "form": { + "mode": "Mode", + "mode_simple": "Simple (days)", + "mode_solver": "Solver (ship target)", "origin": "Точка отправления", "target_days": "Целевой запас", "buy_from_cx": "Купить в CX", "buy_from_cx_warning": "Закупка доступна только со склада.", "fit_ship": "Загрузить корабль", + "ship_weight": "Ship Weight (t)", + "ship_volume": "Ship Volume (m³)", + "full_cover_threshold": "Full Cover", + "full_cover_threshold_info": "Materials whose daily burn is at or below this value are shipped to their full target-days quantity first, before the solver balances the rest of the cargo. This guarantees tiny-burn items are not starved by the optimization. Set to 0 to disable.", "hide_infinite": "Скрыть бесконечные", "json": "JSON" }, diff --git a/src/locales/zh_CN/xit.json b/src/locales/zh_CN/xit.json index 40b0aabb..f8339ade 100644 --- a/src/locales/zh_CN/xit.json +++ b/src/locales/zh_CN/xit.json @@ -1,10 +1,17 @@ { "form": { + "mode": "Mode", + "mode_simple": "Simple (days)", + "mode_solver": "Solver (ship target)", "origin": "来源地", "target_days": "目标天数", "buy_from_cx": "从交易所购买", "buy_from_cx_warning": "只有来源地为仓库时才能采购。", "fit_ship": "适配飞船", + "ship_weight": "Ship Weight (t)", + "ship_volume": "Ship Volume (m³)", + "full_cover_threshold": "Full Cover", + "full_cover_threshold_info": "Materials whose daily burn is at or below this value are shipped to their full target-days quantity first, before the solver balances the rest of the cargo. This guarantees tiny-burn items are not starved by the optimization. Set to 0 to disable.", "hide_infinite": "隐藏无限项", "json": "JSON" }, diff --git a/src/tests/features/xit/useBurnSolver.test.ts b/src/tests/features/xit/useBurnSolver.test.ts new file mode 100644 index 00000000..c7669ffb --- /dev/null +++ b/src/tests/features/xit/useBurnSolver.test.ts @@ -0,0 +1,327 @@ +import { describe, it, expect } from "vitest"; + +import { + buildBurnDemand, + minDays, + prefillGuaranteed, + solveBurn, +} from "@/features/xit/useBurnSolver"; +import { IBurnDemandItem } from "@/features/xit/xitAction.types"; +import { IXITActionElement } from "@/features/xit/xitAction.types"; +import { IMaterial } from "@/features/api/gameData.types"; + +function makeItem( + overrides: Partial & { ticker: string } +): IBurnDemandItem { + return { + burnPerDay: 10, + targetDays: 14, + targetQuantity: 140, + currentStock: 0, + need: 140, + weightPerUnit: 1, + volumePerUnit: 1, + priority: 1, + ...overrides, + }; +} + +function totalWeightVolume( + selected: Map, + items: IBurnDemandItem[] +): { weight: number; volume: number } { + const byTicker = new Map(items.map((i) => [i.ticker, i])); + let weight = 0; + let volume = 0; + for (const [ticker, qty] of selected) { + const item = byTicker.get(ticker); + if (!item) continue; + weight += qty * item.weightPerUnit; + volume += qty * item.volumePerUnit; + } + return { weight, volume }; +} + +describe("useBurnSolver", () => { + it("buildBurnDemand skips surplus materials and builds need from stock", () => { + const elements: IXITActionElement[] = [ + { ticker: "ALO", stock: 20, delta: -2 }, + { ticker: "FEO", stock: 10, delta: 1 }, + ]; + const materialsMap: Record = { + ALO: { + ticker: "ALO", + weight: 1.35, + volume: 1, + name: "Aluminium Ore", + material_id: "1", + category_name: "ores", + category_id: "1", + }, + FEO: { + ticker: "FEO", + weight: 5.9, + volume: 1, + name: "Iron Ore", + material_id: "2", + category_name: "ores", + category_id: "1", + }, + }; + + const demand = buildBurnDemand(elements, materialsMap, 10); + + expect(demand).toHaveLength(1); + expect(demand[0].ticker).toBe("ALO"); + expect(demand[0].burnPerDay).toBe(2); + expect(demand[0].targetQuantity).toBe(20); + expect(demand[0].need).toBe(0); + }); + + it("prefillGuaranteed fully covers tiny-burn materials first", () => { + const items: IBurnDemandItem[] = [ + makeItem({ + ticker: "HSS", + burnPerDay: 0.06, + targetDays: 60, + targetQuantity: 3.6, + need: 3.6, + weightPerUnit: 0.1, + volumePerUnit: 0.1, + }), + makeItem({ + ticker: "PE", + burnPerDay: 100, + targetQuantity: 1400, + need: 1400, + }), + ]; + + const prefill = prefillGuaranteed(items, { + volumeCapacity: 1000, + weightCapacity: 1000, + integer: true, + thresholdBurn: 1.0, + }); + + expect(prefill.guaranteed.has("HSS")).toBe(true); + expect(prefill.selected.get("HSS")).toBe(4); + expect(prefill.usedWeight).toBeCloseTo(0.4); + expect(prefill.usedVolume).toBeCloseTo(0.4); + }); + + it("solveBurn balances minimum days under tight capacity", () => { + const items: IBurnDemandItem[] = [ + makeItem({ + ticker: "AL", + burnPerDay: 10, + targetQuantity: 140, + need: 140, + currentStock: 0, + }), + makeItem({ + ticker: "SI", + burnPerDay: 5, + targetQuantity: 70, + need: 70, + currentStock: 0, + }), + ]; + + const selected = solveBurn(items, { + volumeCapacity: 50, + weightCapacity: 50, + targetDays: 14, + fullCoverBelowBurnPerDay: 0, + }); + + const daysA = + (items[0].currentStock + (selected.get("AL") ?? 0)) / + items[0].burnPerDay; + const daysB = + (items[1].currentStock + (selected.get("SI") ?? 0)) / + items[1].burnPerDay; + + expect(Math.abs(daysA - daysB)).toBeLessThanOrEqual(1.1); + expect(minDays(selected, items)).toBeGreaterThan(0); + }); + + it("solveBurn never exceeds ship capacity", () => { + const items: IBurnDemandItem[] = [ + makeItem({ + ticker: "AL", + burnPerDay: 10, + targetQuantity: 140, + need: 140, + }), + makeItem({ + ticker: "SI", + burnPerDay: 8, + targetQuantity: 112, + need: 112, + }), + makeItem({ + ticker: "FE", + burnPerDay: 0.5, + targetQuantity: 7, + need: 7, + weightPerUnit: 0.2, + volumePerUnit: 0.2, + }), + ]; + + const weightCapacity = 75; + const volumeCapacity = 60; + const selected = solveBurn(items, { + volumeCapacity, + weightCapacity, + targetDays: 14, + fullCoverBelowBurnPerDay: 1.0, + }); + + const totals = totalWeightVolume(selected, items); + expect(totals.weight).toBeLessThanOrEqual(weightCapacity + 1e-6); + expect(totals.volume).toBeLessThanOrEqual(volumeCapacity + 1e-6); + }); +}); + +describe("useBurnSolver super-tiny weight/volume items", () => { + // Weight/volume per unit sourced from the PRUNplanner materials catalog. + // SCN and TRN are the "super-tiny" items at 0.001 t / 0.001 m3 per unit. + const materialData: Record< + string, + { weight: number; volume: number } + > = { + BCO: { weight: 0.005, volume: 0.002 }, + BGO: { weight: 19.32, volume: 1 }, + C: { weight: 2.25, volume: 1 }, + COF: { weight: 0.1, volume: 0.1 }, + DW: { weight: 0.1, volume: 0.1 }, + HMS: { weight: 0.05, volume: 0.05 }, + LST: { weight: 2.73, volume: 1 }, + MED: { weight: 0.3, volume: 0.1 }, + N: { weight: 0.807, volume: 1 }, + PWO: { weight: 0.05, volume: 0.05 }, + RAT: { weight: 0.21, volume: 0.1 }, + RCO: { weight: 0.95, volume: 1 }, + SC: { weight: 0.04, volume: 0.01 }, + SIO: { weight: 1.79, volume: 1 }, + STL: { weight: 7.85, volume: 1 }, + TRN: { weight: 0.001, volume: 0.001 }, + SCN: { weight: 0.001, volume: 0.001 }, + }; + + // The reported plan: buy quantities for a 1000 m3 / 3000 t ship. These sum + // to ~999.999 m3 and ~2499 t, so volume is the binding constraint and the + // list includes a single tiny SCN unit. + const planQuantities: Record = { + BCO: 336, + BGO: 66, + C: 122, + COF: 17, + DW: 190, + HMS: 1, + LST: 62, + MED: 34, + N: 645, + PWO: 1, + RAT: 186, + RCO: 15, + SC: 31, + SIO: 31, + STL: 15, + TRN: 516, + SCN: 1, + }; + + const materialsMap: Record = Object.fromEntries( + Object.entries(materialData).map(([ticker, wv]) => [ + ticker, + { + material_id: ticker, + category_name: "test", + category_id: "test", + name: ticker, + ticker, + weight: wv.weight, + volume: wv.volume, + }, + ]) + ); + + // One day of burn equal to the reported plan quantity, no stock on hand, + // so each material"s `need` equals its plan quantity. + const elements: IXITActionElement[] = Object.entries( + planQuantities + ).map(([ticker, qty]) => ({ ticker, stock: 0, delta: -qty })); + + const VOLUME_CAPACITY = 1000; + const WEIGHT_CAPACITY = 3000; + + function loadedTotals(selected: Map): { + weight: number; + volume: number; + } { + let weight = 0; + let volume = 0; + for (const [ticker, qty] of selected) { + const wv = materialData[ticker]; + if (!wv) continue; + weight += qty * wv.weight; + volume += qty * wv.volume; + } + return { weight, volume }; + } + + it("never exceeds ship volume/weight even with 0.001 items", () => { + const demand = buildBurnDemand(elements, materialsMap, 1); + const selected = solveBurn(demand, { + volumeCapacity: VOLUME_CAPACITY, + weightCapacity: WEIGHT_CAPACITY, + targetDays: 1, + fullCoverBelowBurnPerDay: 1.0, + integer: true, + }); + + const totals = loadedTotals(selected); + expect(totals.volume).toBeLessThanOrEqual(VOLUME_CAPACITY + 1e-6); + expect(totals.weight).toBeLessThanOrEqual(WEIGHT_CAPACITY + 1e-6); + + // integer quantities only + for (const qty of selected.values()) { + expect(Number.isInteger(qty)).toBe(true); + } + }); + + it("covers super-tiny items (SCN/TRN) rather than starving them", () => { + const demand = buildBurnDemand(elements, materialsMap, 1); + const selected = solveBurn(demand, { + volumeCapacity: VOLUME_CAPACITY, + weightCapacity: WEIGHT_CAPACITY, + targetDays: 1, + fullCoverBelowBurnPerDay: 1.0, + integer: true, + }); + + // SCN (burn 0.3/day <= threshold) is guaranteed by the Stage 0 prefill + expect(selected.get("SCN")).toBe(1); + // TRN (burn 633.49/day > threshold) is loaded by the optimizer. + // It fills at the same rate as other materials. + expect(selected.get("TRN")).toBe(516); + }); + + it("fills the binding volume constraint close to capacity", () => { + const demand = buildBurnDemand(elements, materialsMap, 1); + const selected = solveBurn(demand, { + volumeCapacity: VOLUME_CAPACITY, + weightCapacity: WEIGHT_CAPACITY, + targetDays: 1, + fullCoverBelowBurnPerDay: 1.0, + integer: true, + }); + + const totals = loadedTotals(selected); + // The whole plan fits (~999.999 m3), so volume utilization is ~100%. + expect(totals.volume).toBeGreaterThan(999); + }); +}); diff --git a/src/tests/features/xit/useXITBurnAction.test.ts b/src/tests/features/xit/useXITBurnAction.test.ts index c12017ba..b08b9d22 100644 --- a/src/tests/features/xit/useXITBurnAction.test.ts +++ b/src/tests/features/xit/useXITBurnAction.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect, beforeAll } from "vitest"; import { flushPromises } from "@vue/test-utils"; // Stores -import { materialsStore } from "@/database/stores"; +import { materialsStore, exchangesStore } from "@/database/stores"; import { useMaterialData } from "@/database/services/useMaterialData"; // Composables @@ -15,12 +15,15 @@ import { IXITActionElement } from "@/features/xit/xitAction.types"; // test data import materials from "@/tests/test_data/api_data_materials.json"; +import exchanges from "@/tests/test_data/api_data_exchanges.json"; describe("useBurnXITAction", async () => { beforeAll(async () => { setActivePinia(createPinia()); await materialsStore.setMany(materials); + // @ts-expect-error mock data date as string + await exchangesStore.setMany(exchanges); const { preload } = useMaterialData(); @@ -64,6 +67,7 @@ describe("useBurnXITAction", async () => { it("materialTable", async () => { const { materialTable } = await useBurnXITAction( + ref("simple"), ref(elements), ref(resupplyDays), ref(hideInfinite), @@ -81,6 +85,7 @@ describe("useBurnXITAction", async () => { it("totalWeightVolume", async () => { const { totalWeightVolume } = await useBurnXITAction( + ref("simple"), ref(elements), ref(resupplyDays), ref(hideInfinite), @@ -98,6 +103,7 @@ describe("useBurnXITAction", async () => { const days = ref(5); const { fit } = await useBurnXITAction( + ref("simple"), ref(elements), days, ref(hideInfinite), @@ -117,4 +123,66 @@ describe("useBurnXITAction", async () => { fit(50, 50); expect(days.value).toBe(7); }); + + it("solverMode uses ship capacity instead of uniform day scaling", async () => { + const { materialTable, totalWeightVolume } = await useBurnXITAction( + ref("solver"), + ref(elements), + ref(14), // resupply days + ref(false), + ref({}), + ref(new Set()), + ref(undefined), + ref(undefined), + ref(20), // ship weight capacity + ref(20), // ship volume capacity + ref(1.0) + ); + + const loaded = materialTable.value.filter( + (m) => m.active && m.total > 0 && m.delta < 0 + ); + + // Assert that the loaded materials are within the ship capacity + expect(loaded.length).toBeGreaterThan(0); + expect(totalWeightVolume.value.totalWeight).toBeLessThanOrEqual(20); + expect(totalWeightVolume.value.totalVolume).toBeLessThanOrEqual(20); + + // Assert that the simple totals are less than the uniform need + const simpleTotals = loaded.reduce( + (sum, m) => sum + m.total, + 0 + ); + const uniformNeed = loaded.reduce((sum, m) => { + const need = Math.max( + 0, + Math.ceil(m.delta * -1 * 14 - m.stock) + ); + return sum + need; + }, 0); + expect(simpleTotals).toBeLessThan(uniformNeed); + }); + + it("solverMode excludes inactive materials from solver demand", async () => { + const { materialTable } = await useBurnXITAction( + ref("solver"), + ref(elements), + ref(14), + ref(false), + ref({}), + ref(new Set(["LST"])), + ref(undefined), + ref(undefined), + ref(1000), + ref(1000), + ref(1.0) + ); + + const inactiveMaterial = materialTable.value.find( + (m) => m.ticker === "LST" + ); + + expect(inactiveMaterial?.active).toBe(false); + expect(inactiveMaterial?.total).toBe(0); + }); }); From 332bd550e16711cb04530bdb58a1f44641de3cea Mon Sep 17 00:00:00 2001 From: theit8514 Date: Sun, 7 Jun 2026 12:10:14 -0400 Subject: [PATCH 2/4] feat(user): persist burn mode --- src/features/api/schemas/user.schemas.ts | 6 ++++ src/features/preferences/usePreferences.ts | 16 +++++++++ src/features/preferences/userDefaults.ts | 2 ++ .../preferences/userPreferences.types.ts | 2 ++ .../profile/components/UserPreferences.vue | 34 +++++++++++++++++++ .../xit/components/XITBurnActionButton.vue | 14 ++++---- src/locales/de_DE/profile.json | 2 ++ src/locales/en_US/profile.json | 2 ++ src/locales/es_ES/profile.json | 2 ++ src/locales/fr_FR/profile.json | 2 ++ src/locales/it_IT/profile.json | 2 ++ src/locales/ja_JP/profile.json | 2 ++ src/locales/ko_KR/profile.json | 2 ++ src/locales/nl_NL/profile.json | 2 ++ src/locales/pt_PT/profile.json | 2 ++ src/locales/ru_RU/profile.json | 2 ++ src/locales/zh_CN/profile.json | 2 ++ .../preferences/usePreferences.test.ts | 30 ++++++++++++++++ 18 files changed, 119 insertions(+), 7 deletions(-) diff --git a/src/features/api/schemas/user.schemas.ts b/src/features/api/schemas/user.schemas.ts index 7f26273b..360a0529 100644 --- a/src/features/api/schemas/user.schemas.ts +++ b/src/features/api/schemas/user.schemas.ts @@ -136,6 +136,12 @@ export const UserPreferenceSchema: z.ZodType = z.object({ burnDaysYellow: z.number(), burnResupplyDays: z.number(), burnOrigin: z.string(), + burnDefaultMode: z + .preprocess((v) => v ?? "simple", z.enum(["simple", "solver"])) + .catch("simple"), + burnFullCoverThreshold: z + .preprocess((v) => v ?? 1.0, z.number()) + .catch(1.0), layoutNavigationStyle: z.enum(["full", "collapsed"]), planOverrides: z.record( z.string(), diff --git a/src/features/preferences/usePreferences.ts b/src/features/preferences/usePreferences.ts index bd5fd8c4..bac4021b 100644 --- a/src/features/preferences/usePreferences.ts +++ b/src/features/preferences/usePreferences.ts @@ -92,6 +92,20 @@ export function usePreferences() { set: (v) => userStore.setPreference("burnOrigin", v), }); + const burnDefaultMode: WritableComputedRef< + "simple" | "solver", + "simple" | "solver" + > = computed<"simple" | "solver">({ + get: () => userStore.preferences.burnDefaultMode, + set: (v) => userStore.setPreference("burnDefaultMode", v), + }); + + const burnFullCoverThreshold: WritableComputedRef = + computed({ + get: () => userStore.preferences.burnFullCoverThreshold, + set: (v) => userStore.setPreference("burnFullCoverThreshold", v), + }); + const planSettings: ComputedRef< Record> > = computed(() => { @@ -234,6 +248,8 @@ export function usePreferences() { burnDaysYellow, burnResupplyDays, burnOrigin, + burnDefaultMode, + burnFullCoverThreshold, planSettings, planSettingsOverview, layoutNavigationStyle, diff --git a/src/features/preferences/userDefaults.ts b/src/features/preferences/userDefaults.ts index 3405be1a..c0e11ada 100644 --- a/src/features/preferences/userDefaults.ts +++ b/src/features/preferences/userDefaults.ts @@ -17,6 +17,8 @@ export const preferenceDefaults: IPreferenceDefault = { burnDaysYellow: 10, burnResupplyDays: 20, burnOrigin: "Configure on Execution", + burnDefaultMode: "simple", + burnFullCoverThreshold: 1.0, layoutNavigationStyle: "full", planOverrides: {}, diff --git a/src/features/preferences/userPreferences.types.ts b/src/features/preferences/userPreferences.types.ts index f744d4d5..ce283a3e 100644 --- a/src/features/preferences/userPreferences.types.ts +++ b/src/features/preferences/userPreferences.types.ts @@ -15,6 +15,8 @@ export interface IPreference { burnDaysYellow: number; burnResupplyDays: number; burnOrigin: string; + burnDefaultMode: "simple" | "solver"; + burnFullCoverThreshold: number; layoutNavigationStyle: "full" | "collapsed"; // seeding per plan defaults diff --git a/src/features/profile/components/UserPreferences.vue b/src/features/profile/components/UserPreferences.vue index e7dfbe22..97ebcb54 100644 --- a/src/features/profile/components/UserPreferences.vue +++ b/src/features/profile/components/UserPreferences.vue @@ -28,7 +28,9 @@ PInputNumber, PCheckbox, PTable, + PTooltip, } from "@/ui"; + import { HelpOutlineSharp } from "@vicons/material"; const planningStore = usePlanningStore(); @@ -37,10 +39,17 @@ burnDaysYellow, burnResupplyDays, burnOrigin, + burnDefaultMode, + burnFullCoverThreshold, locale, planSettingsOverview, cleanPlanPreferences, } = usePreferences(); + + const burnModeOptions: Ref = ref([ + { label: t("xit.form.mode_simple"), value: "simple" }, + { label: t("xit.form.mode_solver"), value: "solver" }, + ]); let { defaultEmpireUuid, defaultCXUuid, defaultBuyItemsFromCX } = usePreferences(); @@ -174,6 +183,31 @@ + + + + +
+ + + +
+ {{ t("xit.form.full_cover_threshold_info") }} +
+
+
+

diff --git a/src/features/xit/components/XITBurnActionButton.vue b/src/features/xit/components/XITBurnActionButton.vue index ccd84468..dad21a95 100644 --- a/src/features/xit/components/XITBurnActionButton.vue +++ b/src/features/xit/components/XITBurnActionButton.vue @@ -13,6 +13,8 @@ const { burnResupplyDays, burnOrigin, + burnDefaultMode, + burnFullCoverThreshold, getBurnDisplayClass, defaultBuyItemsFromCX, } = usePreferences(); @@ -106,10 +108,8 @@ const refHideInfinite: Ref = ref(false); const refMaterialOverrides: Ref> = ref({}); const refMaterialInactives: Ref> = ref(new Set([])); - const refBurnMode: Ref<"simple" | "solver"> = ref("simple"); const refShipWeightCapacity: Ref = ref(1000); const refShipVolumeCapacity: Ref = ref(1000); - const refFullCoverThreshold: Ref = ref(1.0); const modeOptions = computed(() => [ { label: t("xit.form.mode_simple"), value: "simple" }, @@ -118,7 +118,7 @@ const { materialTable, totalWeightVolume, totalPrice, fit } = await useBurnXITAction( - refBurnMode, + burnDefaultMode, localElements, burnResupplyDays, refHideInfinite, @@ -128,7 +128,7 @@ ref(undefined), refShipWeightCapacity, refShipVolumeCapacity, - refFullCoverThreshold + burnFullCoverThreshold ); function applyShipPreset(weight: number, volume: number): void { @@ -152,7 +152,7 @@ @@ -182,7 +182,7 @@ burnOrigin === 'Configure on Execution' " /> -