Show ingredient tier/enchant + price age in haul advisor
All checks were successful
Deploy / deploy (push) Successful in 1m42s

- Opportunities breakdown now shows each ingredient's tier/enchant
  (e.g. T6.4) so enchant mismatches are visible at a glance.
- Travel haul advisor now shows color-coded price-age badges (green
  fresh / yellow aging / red stale) for both buy and sell prices, like
  Opportunities already does.

Note: the enchanted-ingredient pricing itself was already correct in
code (T6_SHOES_PLATE_SET1@4); a deployed DB parsed before that fix must
re-run Game Data -> Re-download to pick it up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Заид Омар Медхат 2026-08-23 05:33:52 +05:00
parent 38c7ff5e56
commit 7ed585d6a9
3 changed files with 31 additions and 2 deletions

View file

@ -141,6 +141,8 @@ export interface HaulItem {
weight: number; weight: number;
buyAt: number; // sell-order price in origin (you buy instantly) buyAt: number; // sell-order price in origin (you buy instantly)
sellAt: number; // sell-order price in destination sellAt: number; // sell-order price in destination
buyAgeHours: number | null; // how stale the origin price is
sellAgeHours: number | null; // how stale the destination price is
netPerUnit: number; // after taxes at destination netPerUnit: number; // after taxes at destination
unitsCarryable: number; unitsCarryable: number;
tripProfit: number; tripProfit: number;
@ -177,6 +179,8 @@ export async function haulAdvisor(from: string, to: string, maxWeight: number, m
weight: it.weight, weight: it.weight,
buyAt: buy.price, buyAt: buy.price,
sellAt: sell.price, sellAt: sell.price,
buyAgeHours: buy.ageHours,
sellAgeHours: sell.ageHours,
netPerUnit, netPerUnit,
unitsCarryable, unitsCarryable,
tripProfit: netPerUnit * unitsCarryable, tripProfit: netPerUnit * unitsCarryable,

View file

@ -41,6 +41,14 @@ const CITIES = ['Bridgewatch', 'Caerleon', 'Fort Sterling', 'Lymhurst', 'Martloc
const silver = (n: number) => Math.round(n).toLocaleString('en-US'); const silver = (n: number) => Math.round(n).toLocaleString('en-US');
/** Derive a "T6.4" chip from an item id like "T6_SHOES_PLATE_SET1@4" (blank for non-tiered ids). */
function tierEnchant(id: string): string {
const t = id.match(/^T(\d)_/);
if (!t) return '';
const e = id.match(/@(\d)$/);
return `T${t[1]}${e ? `.${e[1]}` : ''}`;
}
function Age({ hours }: { hours: number | null }) { function Age({ hours }: { hours: number | null }) {
if (hours == null) return null; if (hours == null) return null;
const label = hours < 1 ? `${Math.round(hours * 60)}m` : hours < 48 ? `${Math.round(hours)}h` : `${Math.round(hours / 24)}d`; const label = hours < 1 ? `${Math.round(hours * 60)}m` : hours < 48 ? `${Math.round(hours)}h` : `${Math.round(hours / 24)}d`;
@ -218,7 +226,12 @@ export default function OpportunitiesPage() {
<tbody> <tbody>
{o.inputs.map((inp) => ( {o.inputs.map((inp) => (
<tr key={inp.item}> <tr key={inp.item}>
<td>{o.inputNames[inp.item] ?? inp.item}</td> <td>
{o.inputNames[inp.item] ?? inp.item}
{tierEnchant(inp.item) && (
<span className="tag" title={inp.item}>{tierEnchant(inp.item)}</span>
)}
</td>
<td>{inp.count}</td> <td>{inp.count}</td>
<td>{inp.effectiveCount.toFixed(2)}</td> <td>{inp.effectiveCount.toFixed(2)}</td>
<td>{silver(inp.unitCost)}</td> <td>{silver(inp.unitCost)}</td>

View file

@ -26,12 +26,22 @@ interface HaulItem {
weight: number; weight: number;
buyAt: number; buyAt: number;
sellAt: number; sellAt: number;
buyAgeHours: number | null;
sellAgeHours: number | null;
netPerUnit: number; netPerUnit: number;
unitsCarryable: number; unitsCarryable: number;
tripProfit: number; tripProfit: number;
profitPerWeight: number; profitPerWeight: number;
} }
/** Color-coded price-age badge: green fresh, yellow aging, red stale. */
function PriceAge({ hours }: { hours: number | null }) {
if (hours == null) return <span className="muted">—</span>;
const label = hours < 1 ? `${Math.round(hours * 60)}m` : hours < 48 ? `${Math.round(hours)}h` : `${Math.round(hours / 24)}d`;
const color = hours < 6 ? '#7fd18a' : hours < 24 ? '#e0c069' : '#e08869';
return <span style={{ color, fontSize: 12 }}>{label}</span>;
}
interface Status { interface Status {
zones: number; zones: number;
connections: number; connections: number;
@ -218,7 +228,7 @@ export default function TravelPage() {
<table style={{ marginTop: 10 }}> <table style={{ marginTop: 10 }}>
<thead> <thead>
<tr> <tr>
<th>Item</th><th>Buy at {from}</th><th>Sell at {to}</th><th>Net/unit</th><th>Units</th><th>Trip profit</th><th>Profit/kg</th> <th>Item</th><th>Buy at {from}</th><th>age</th><th>Sell at {to}</th><th>age</th><th>Net/unit</th><th>Units</th><th>Trip profit</th><th>Profit/kg</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -226,7 +236,9 @@ export default function TravelPage() {
<tr key={h.item}> <tr key={h.item}>
<td>{h.name ?? h.item} <span className="tag">T{h.tier}</span></td> <td>{h.name ?? h.item} <span className="tag">T{h.tier}</span></td>
<td>{silver(h.buyAt)}</td> <td>{silver(h.buyAt)}</td>
<td><PriceAge hours={h.buyAgeHours} /></td>
<td>{silver(h.sellAt)}</td> <td>{silver(h.sellAt)}</td>
<td><PriceAge hours={h.sellAgeHours} /></td>
<td>{silver(h.netPerUnit)}</td> <td>{silver(h.netPerUnit)}</td>
<td>{h.unitsCarryable}</td> <td>{h.unitsCarryable}</td>
<td style={{ color: '#7fd18a', fontWeight: 600 }}>{silver(h.tripProfit)}</td> <td style={{ color: '#7fd18a', fontWeight: 600 }}>{silver(h.tripProfit)}</td>