@shipload/sdk 2.0.0-rc5 → 2.0.0-rc6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/shipload.d.ts +376 -1008
- package/lib/shipload.js +712 -1948
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +694 -1924
- package/lib/shipload.m.js.map +1 -1
- package/package.json +1 -1
- package/src/capabilities/crafting.ts +10 -0
- package/src/capabilities/guards.ts +0 -5
- package/src/capabilities/index.ts +1 -0
- package/src/capabilities/storage.ts +0 -8
- package/src/contracts/server.ts +103 -220
- package/src/data/items.json +15 -15
- package/src/data/recipes.ts +129 -0
- package/src/derivation/crafting.ts +120 -0
- package/src/derivation/index.ts +1 -0
- package/src/derivation/stats.ts +91 -15
- package/src/derivation/stratum.ts +2 -2
- package/src/entities/cargo-utils.ts +6 -64
- package/src/entities/container.ts +18 -0
- package/src/entities/entity-inventory.ts +0 -4
- package/src/entities/inventory-accessor.ts +0 -4
- package/src/entities/location.ts +2 -197
- package/src/entities/player.ts +1 -274
- package/src/entities/ship.ts +0 -21
- package/src/entities/warehouse.ts +0 -4
- package/src/index-module.ts +34 -41
- package/src/managers/actions.ts +38 -90
- package/src/managers/context.ts +0 -9
- package/src/managers/index.ts +0 -1
- package/src/managers/locations.ts +2 -85
- package/src/market/items.ts +0 -1
- package/src/scheduling/projection.ts +0 -10
- package/src/shipload.ts +0 -5
- package/src/types/capabilities.ts +1 -9
- package/src/types/entity-traits.ts +3 -4
- package/src/types/entity.ts +0 -1
- package/src/types.ts +5 -25
- package/src/utils/system.ts +5 -4
- package/src/managers/trades.ts +0 -119
- package/src/market/market.ts +0 -195
- package/src/market/rolls.ts +0 -8
- package/src/trading/collect.ts +0 -938
- package/src/trading/deal.ts +0 -207
- package/src/trading/trade.ts +0 -203
package/src/trading/collect.ts
DELETED
|
@@ -1,938 +0,0 @@
|
|
|
1
|
-
import {Checksum256Type, Int64, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
|
|
2
|
-
import {Ship} from '../entities/ship'
|
|
3
|
-
import {Location} from '../entities/location'
|
|
4
|
-
import {Coordinates, ItemPrice, ShipLike} from '../types'
|
|
5
|
-
import {Deal, findDealsForShip} from './deal'
|
|
6
|
-
|
|
7
|
-
import {EntityInventory} from '../entities/entity-inventory'
|
|
8
|
-
import {
|
|
9
|
-
calc_loader_flighttime,
|
|
10
|
-
distanceBetweenCoordinates,
|
|
11
|
-
EstimatedTravelTime,
|
|
12
|
-
estimateTravelTime,
|
|
13
|
-
} from '../travel/travel'
|
|
14
|
-
import {getItem, getItems} from '../market/items'
|
|
15
|
-
import {getRarity, Rarities} from '../market/market'
|
|
16
|
-
import {ServerContract} from '../contracts'
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Calculate the mass of cargo (based on quantity).
|
|
20
|
-
* Used for estimating unload time.
|
|
21
|
-
*/
|
|
22
|
-
function calculateCargoMass(cargo: EntityInventory[]): UInt32 {
|
|
23
|
-
let mass = UInt32.from(0)
|
|
24
|
-
for (const c of cargo) {
|
|
25
|
-
if (UInt64.from(c.quantity).gt(UInt64.zero)) {
|
|
26
|
-
const goodMass = getItem(c.item_id).mass
|
|
27
|
-
mass = mass.adding(goodMass.multiplying(c.quantity))
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return mass
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function calculateUnloadTime(
|
|
34
|
-
ship: ServerContract.Types.entity_info,
|
|
35
|
-
cargo: EntityInventory[]
|
|
36
|
-
): UInt32 {
|
|
37
|
-
const unloadMass = calculateCargoMass(cargo)
|
|
38
|
-
if (
|
|
39
|
-
unloadMass.equals(UInt32.zero) ||
|
|
40
|
-
!ship.loaders ||
|
|
41
|
-
ship.loaders.quantity.equals(UInt32.zero)
|
|
42
|
-
) {
|
|
43
|
-
return UInt32.zero
|
|
44
|
-
}
|
|
45
|
-
const totalMass = UInt64.from(unloadMass).adding(ship.loaders.mass)
|
|
46
|
-
return calc_loader_flighttime(ship as ShipLike, totalMass).dividing(ship.loaders.quantity)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Types of collect actions available to the player
|
|
51
|
-
*/
|
|
52
|
-
export type CollectActionType =
|
|
53
|
-
| 'sell-and-trade' // arrive/sell/buy/travel - full loop
|
|
54
|
-
| 'sell-and-reposition' // arrive/sell/refuel/travel - sell here, travel empty to deals
|
|
55
|
-
| 'travel-to-sell' // arrive/refuel/travel - keep cargo, sell at better location
|
|
56
|
-
| 'sell-and-stay' // arrive/sell - just sell, stay idle
|
|
57
|
-
| 'explore' // arrive/refuel/travel - travel to find opportunities
|
|
58
|
-
| 'orbit' // just arrive, keep cargo, decide later
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Represents a single collect option presented to the player
|
|
62
|
-
*/
|
|
63
|
-
export interface CollectOption {
|
|
64
|
-
/** Unique identifier for this option */
|
|
65
|
-
id: string
|
|
66
|
-
/** Type of action sequence */
|
|
67
|
-
type: CollectActionType
|
|
68
|
-
/** Human-readable title */
|
|
69
|
-
title: string
|
|
70
|
-
/** Detailed description of what will happen */
|
|
71
|
-
description: string
|
|
72
|
-
/** Brief explanation of why this option is worth considering */
|
|
73
|
-
reason: string
|
|
74
|
-
/** Whether this is the recommended option (best profitPerSecond with quality threshold) */
|
|
75
|
-
recommended: boolean
|
|
76
|
-
/** Whether this option has the highest absolute profit (may differ from recommended) */
|
|
77
|
-
highestProfit: boolean
|
|
78
|
-
/** Estimated profit/loss from this action */
|
|
79
|
-
estimatedProfit: UInt64
|
|
80
|
-
/** Revenue from selling cargo (if applicable) */
|
|
81
|
-
saleRevenue?: UInt64
|
|
82
|
-
/** Cost of purchasing new cargo (if applicable) */
|
|
83
|
-
purchaseCost?: UInt64
|
|
84
|
-
/** Expected profit from the next trade (if applicable) */
|
|
85
|
-
nextTradeProfit?: UInt64
|
|
86
|
-
/** Profit per second for this option (floating point for display) */
|
|
87
|
-
profitPerSecond?: number
|
|
88
|
-
/** Margin percentage for the deal (floating point for display) */
|
|
89
|
-
marginPercent?: number
|
|
90
|
-
/** Destination location (if traveling) */
|
|
91
|
-
destination?: Location
|
|
92
|
-
/** Deal to execute (if buying goods) */
|
|
93
|
-
deal?: Deal
|
|
94
|
-
/** Sale location if different from current */
|
|
95
|
-
saleLocation?: Location
|
|
96
|
-
/** Price per unit at sale location */
|
|
97
|
-
salePrice?: UInt32
|
|
98
|
-
/** Price per unit at current location (for comparison) */
|
|
99
|
-
currentPrice?: UInt32
|
|
100
|
-
/** Estimated travel time in seconds (undefined = instant/no travel) */
|
|
101
|
-
travelTime?: UInt32
|
|
102
|
-
/** Detailed breakdown of travel time components */
|
|
103
|
-
travelTimeBreakdown?: EstimatedTravelTime
|
|
104
|
-
/** Info about a discounted good at the destination (for explore options) */
|
|
105
|
-
discountedGood?: DiscountedItemInfo
|
|
106
|
-
/** Top potential deals available at destination (for explore options) */
|
|
107
|
-
potentialDeals?: PotentialDeal[]
|
|
108
|
-
/** Details of cargo being sold (if selling cargo) */
|
|
109
|
-
cargoSale?: CargoSaleItem[]
|
|
110
|
-
/** Total profit/loss from selling cargo */
|
|
111
|
-
cargoProfitLoss?: Int64
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Analysis result for collect options
|
|
116
|
-
*/
|
|
117
|
-
export interface CollectAnalysis {
|
|
118
|
-
/** Current location where ship arrived */
|
|
119
|
-
arrivedAt: Coordinates
|
|
120
|
-
/** Ship being analyzed */
|
|
121
|
-
ship: Ship
|
|
122
|
-
/** Current cargo on ship */
|
|
123
|
-
cargo: EntityInventory[]
|
|
124
|
-
/** Value of cargo if sold at current location */
|
|
125
|
-
cargoValueHere: UInt64
|
|
126
|
-
/** All available options, sorted by estimated profit */
|
|
127
|
-
options: CollectOption[]
|
|
128
|
-
/** Whether any profitable options exist */
|
|
129
|
-
hasProfitableOptions: boolean
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Options for analyzing collect choices
|
|
134
|
-
*/
|
|
135
|
-
export interface CollectAnalysisOptions {
|
|
136
|
-
/** Player's current balance (defaults to Infinity) */
|
|
137
|
-
playerBalance?: number
|
|
138
|
-
/** Maximum distance to search (defaults to ship's max range) */
|
|
139
|
-
maxDistance?: number
|
|
140
|
-
/** Minimum profit improvement to suggest traveling elsewhere to sell */
|
|
141
|
-
minSaleImprovement?: number
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Find locations where current cargo could be sold for more
|
|
146
|
-
*/
|
|
147
|
-
export interface BetterSaleLocation {
|
|
148
|
-
location: Location
|
|
149
|
-
/** Price per unit at this location */
|
|
150
|
-
price: UInt32
|
|
151
|
-
/** Total revenue if sold here */
|
|
152
|
-
revenue: UInt64
|
|
153
|
-
/** Difference vs selling at current location */
|
|
154
|
-
improvement: Int64
|
|
155
|
-
/** Best deal available at this location after selling */
|
|
156
|
-
bestDealAfterSale?: Deal
|
|
157
|
-
/** Distance to this location */
|
|
158
|
-
distance: UInt64
|
|
159
|
-
/** Estimated travel time */
|
|
160
|
-
travelTime: UInt32
|
|
161
|
-
/** Detailed breakdown of travel time components */
|
|
162
|
-
travelTimeBreakdown?: EstimatedTravelTime
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Find locations with good deals when current location has none
|
|
167
|
-
*/
|
|
168
|
-
export interface RepositionLocation {
|
|
169
|
-
location: Location
|
|
170
|
-
/** Best deal available at this location */
|
|
171
|
-
bestDeal: Deal
|
|
172
|
-
/** Distance to this location */
|
|
173
|
-
distance: UInt64
|
|
174
|
-
/** Estimated travel time */
|
|
175
|
-
travelTime: UInt32
|
|
176
|
-
/** Detailed breakdown of travel time components */
|
|
177
|
-
travelTimeBreakdown?: EstimatedTravelTime
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Analyze cargo sale value at a specific location
|
|
182
|
-
*/
|
|
183
|
-
export function analyzeCargoSale(
|
|
184
|
-
cargo: EntityInventory[],
|
|
185
|
-
prices: Map<number, UInt64>
|
|
186
|
-
): {revenue: UInt64; cost: UInt64; profit: Int64} {
|
|
187
|
-
let revenue = UInt64.zero
|
|
188
|
-
let cost = UInt64.zero
|
|
189
|
-
|
|
190
|
-
for (const c of cargo) {
|
|
191
|
-
if (UInt64.from(c.quantity).equals(UInt64.zero)) continue
|
|
192
|
-
|
|
193
|
-
const goodId = Number(c.item_id)
|
|
194
|
-
const salePrice = prices.get(goodId)
|
|
195
|
-
|
|
196
|
-
if (salePrice) {
|
|
197
|
-
revenue = revenue.adding(UInt64.from(salePrice).multiplying(c.quantity))
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
cost = cost.adding(c.unit_cost.multiplying(c.quantity))
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
revenue,
|
|
205
|
-
cost,
|
|
206
|
-
profit: Int64.from(revenue).subtracting(cost),
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Build cargo sale item details for UI display
|
|
212
|
-
*/
|
|
213
|
-
export function buildCargoSaleItems(
|
|
214
|
-
cargo: EntityInventory[],
|
|
215
|
-
prices: Map<number, UInt64>
|
|
216
|
-
): CargoSaleItem[] {
|
|
217
|
-
const items: CargoSaleItem[] = []
|
|
218
|
-
|
|
219
|
-
for (const c of cargo) {
|
|
220
|
-
if (UInt64.from(c.quantity).equals(UInt64.zero)) continue
|
|
221
|
-
|
|
222
|
-
const goodId = Number(c.item_id)
|
|
223
|
-
const salePrice = prices.get(goodId)
|
|
224
|
-
const pricePerUnit = salePrice ? UInt32.from(salePrice) : UInt32.zero
|
|
225
|
-
const revenue = UInt64.from(pricePerUnit).multiplying(c.quantity)
|
|
226
|
-
const cost = c.unit_cost.multiplying(c.quantity)
|
|
227
|
-
const profit = Int64.from(revenue).subtracting(cost)
|
|
228
|
-
|
|
229
|
-
items.push({
|
|
230
|
-
goodId: c.item_id,
|
|
231
|
-
goodName: c.item?.name ?? `Item #${goodId}`,
|
|
232
|
-
quantity: UInt32.from(c.quantity),
|
|
233
|
-
pricePerUnit,
|
|
234
|
-
revenue,
|
|
235
|
-
costPerUnit: c.unit_cost,
|
|
236
|
-
profit,
|
|
237
|
-
})
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return items
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Create a "Sell & Trade" option (full loop)
|
|
245
|
-
*/
|
|
246
|
-
export function createSellAndTradeOption(
|
|
247
|
-
saleRevenue: UInt64,
|
|
248
|
-
saleCost: UInt64,
|
|
249
|
-
deal: Deal,
|
|
250
|
-
cargoSale?: CargoSaleItem[],
|
|
251
|
-
unloadTime?: UInt32
|
|
252
|
-
): CollectOption {
|
|
253
|
-
const saleProfit = Int64.from(saleRevenue).subtracting(saleCost)
|
|
254
|
-
const totalProfit = saleProfit.adding(deal.totalProfit)
|
|
255
|
-
const profitPerSecond = deal.travelTime.gt(UInt32.zero)
|
|
256
|
-
? Number(totalProfit) / Number(deal.travelTime)
|
|
257
|
-
: Number(totalProfit)
|
|
258
|
-
|
|
259
|
-
const unload = unloadTime ?? UInt32.zero
|
|
260
|
-
const breakdown: EstimatedTravelTime | undefined = deal.travelTimeBreakdown
|
|
261
|
-
? {
|
|
262
|
-
unloadTime: unload,
|
|
263
|
-
loadTime: deal.travelTimeBreakdown.loadTime,
|
|
264
|
-
rechargeTime: deal.travelTimeBreakdown.rechargeTime,
|
|
265
|
-
flightTime: deal.travelTimeBreakdown.flightTime,
|
|
266
|
-
total: unload
|
|
267
|
-
.adding(deal.travelTimeBreakdown.loadTime)
|
|
268
|
-
.adding(deal.travelTimeBreakdown.rechargeTime)
|
|
269
|
-
.adding(deal.travelTimeBreakdown.flightTime),
|
|
270
|
-
}
|
|
271
|
-
: undefined
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
id: `sell-trade-${deal.destination.coordinates.x}-${deal.destination.coordinates.y}-${deal.item.id}`,
|
|
275
|
-
type: 'sell-and-trade',
|
|
276
|
-
title: `Trade ${deal.item.item.name}`,
|
|
277
|
-
description: `Sell cargo, buy ${deal.maxQuantity} ${deal.item.item.name}, deliver to (${deal.destination.coordinates.x}, ${deal.destination.coordinates.y})`,
|
|
278
|
-
reason: `${deal.marginPercent.toFixed(0)}% margin, ${deal.profitPerSecond.toFixed(
|
|
279
|
-
1
|
|
280
|
-
)}/s profit rate`,
|
|
281
|
-
recommended: false,
|
|
282
|
-
highestProfit: false,
|
|
283
|
-
estimatedProfit: saleProfit,
|
|
284
|
-
saleRevenue,
|
|
285
|
-
purchaseCost: UInt64.from(deal.buyPrice).multiplying(deal.maxQuantity),
|
|
286
|
-
nextTradeProfit: deal.totalProfit,
|
|
287
|
-
profitPerSecond,
|
|
288
|
-
marginPercent: deal.marginPercent,
|
|
289
|
-
destination: deal.destination,
|
|
290
|
-
deal,
|
|
291
|
-
travelTime: breakdown?.total ?? deal.travelTime,
|
|
292
|
-
travelTimeBreakdown: breakdown,
|
|
293
|
-
cargoSale,
|
|
294
|
-
cargoProfitLoss: saleProfit,
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Create a "Travel to Sell" option (better market elsewhere)
|
|
300
|
-
*/
|
|
301
|
-
export function createTravelToSellOption(
|
|
302
|
-
currentRevenue: UInt64,
|
|
303
|
-
cargoCost: UInt64,
|
|
304
|
-
betterSale: BetterSaleLocation,
|
|
305
|
-
cargo: EntityInventory[],
|
|
306
|
-
destPrices?: Map<number, UInt64>
|
|
307
|
-
): CollectOption {
|
|
308
|
-
const totalQuantity = cargo.reduce((s, c) => s.adding(UInt64.from(c.quantity)), UInt64.zero)
|
|
309
|
-
const currentPrice = totalQuantity.gt(UInt64.zero)
|
|
310
|
-
? UInt32.from(currentRevenue.dividing(totalQuantity))
|
|
311
|
-
: UInt32.zero
|
|
312
|
-
const priceIncrease = betterSale.price.gte(currentPrice)
|
|
313
|
-
? betterSale.price.subtracting(currentPrice)
|
|
314
|
-
: UInt32.zero
|
|
315
|
-
const hasDealAfter = !!betterSale.bestDealAfterSale
|
|
316
|
-
|
|
317
|
-
const cargoSale = destPrices ? buildCargoSaleItems(cargo, destPrices) : undefined
|
|
318
|
-
const cargoProfitLoss = cargoSale?.reduce((sum, item) => sum.adding(item.profit), Int64.zero)
|
|
319
|
-
|
|
320
|
-
const saleProfit = Int64.from(betterSale.revenue).subtracting(cargoCost)
|
|
321
|
-
const profitPerSecond = betterSale.travelTime.gt(UInt32.zero)
|
|
322
|
-
? Number(saleProfit) / Number(betterSale.travelTime)
|
|
323
|
-
: Number(saleProfit)
|
|
324
|
-
|
|
325
|
-
return {
|
|
326
|
-
id: `travel-sell-${betterSale.location.coordinates.x}-${betterSale.location.coordinates.y}`,
|
|
327
|
-
type: 'travel-to-sell',
|
|
328
|
-
title: 'Move to Sell Nearby',
|
|
329
|
-
description: `Keep cargo, travel to better market${hasDealAfter ? ', then trade' : ''}`,
|
|
330
|
-
reason: `+${Number(priceIncrease).toLocaleString()}/unit better price${
|
|
331
|
-
hasDealAfter ? ', good deals available there' : ''
|
|
332
|
-
}`,
|
|
333
|
-
recommended: false,
|
|
334
|
-
highestProfit: false,
|
|
335
|
-
estimatedProfit: betterSale.improvement,
|
|
336
|
-
saleRevenue: betterSale.revenue,
|
|
337
|
-
profitPerSecond,
|
|
338
|
-
saleLocation: betterSale.location,
|
|
339
|
-
salePrice: betterSale.price,
|
|
340
|
-
currentPrice,
|
|
341
|
-
destination: betterSale.location,
|
|
342
|
-
deal: betterSale.bestDealAfterSale,
|
|
343
|
-
travelTime: betterSale.travelTime,
|
|
344
|
-
travelTimeBreakdown: betterSale.travelTimeBreakdown,
|
|
345
|
-
cargoSale,
|
|
346
|
-
cargoProfitLoss,
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Create a "Sell & Reposition" option (sell here, travel empty to deals)
|
|
352
|
-
*/
|
|
353
|
-
export function createSellAndRepositionOption(
|
|
354
|
-
saleRevenue: UInt64,
|
|
355
|
-
saleCost: UInt64,
|
|
356
|
-
reposition: RepositionLocation,
|
|
357
|
-
cargoSale?: CargoSaleItem[]
|
|
358
|
-
): CollectOption {
|
|
359
|
-
const saleProfit = Int64.from(saleRevenue).subtracting(saleCost)
|
|
360
|
-
const deal = reposition.bestDeal
|
|
361
|
-
|
|
362
|
-
return {
|
|
363
|
-
id: `sell-reposition-${reposition.location.coordinates.x}-${reposition.location.coordinates.y}`,
|
|
364
|
-
type: 'sell-and-reposition',
|
|
365
|
-
title: 'Sell & Move',
|
|
366
|
-
description: `Sell cargo here, travel empty to buy ${deal.item.item.name}`,
|
|
367
|
-
reason: `No good trades here — ${deal.marginPercent.toFixed(
|
|
368
|
-
0
|
|
369
|
-
)}% margin trade available at destination`,
|
|
370
|
-
recommended: false,
|
|
371
|
-
highestProfit: false,
|
|
372
|
-
estimatedProfit: saleProfit,
|
|
373
|
-
saleRevenue,
|
|
374
|
-
nextTradeProfit: deal.totalProfit,
|
|
375
|
-
profitPerSecond: deal.profitPerSecond,
|
|
376
|
-
marginPercent: deal.marginPercent,
|
|
377
|
-
destination: reposition.location,
|
|
378
|
-
deal: reposition.bestDeal,
|
|
379
|
-
travelTime: reposition.travelTime,
|
|
380
|
-
travelTimeBreakdown: reposition.travelTimeBreakdown,
|
|
381
|
-
cargoSale,
|
|
382
|
-
cargoProfitLoss: saleProfit,
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Create an "Orbit" option (just arrive, keep cargo)
|
|
388
|
-
*/
|
|
389
|
-
export function createOrbitOption(): CollectOption {
|
|
390
|
-
return {
|
|
391
|
-
id: 'orbit',
|
|
392
|
-
type: 'orbit',
|
|
393
|
-
title: 'Enter Orbit',
|
|
394
|
-
description: 'Arrive at this location, keep cargo',
|
|
395
|
-
reason: 'Keep cargo, decide later',
|
|
396
|
-
recommended: false,
|
|
397
|
-
highestProfit: false,
|
|
398
|
-
estimatedProfit: UInt64.zero,
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Create a "Sell & Stay" option (just sell, stay idle)
|
|
404
|
-
*/
|
|
405
|
-
export function createSellAndStayOption(
|
|
406
|
-
saleRevenue: UInt64,
|
|
407
|
-
saleCost: UInt64,
|
|
408
|
-
cargoSale?: CargoSaleItem[],
|
|
409
|
-
unloadTime?: UInt32
|
|
410
|
-
): CollectOption {
|
|
411
|
-
const saleProfit = Int64.from(saleRevenue).subtracting(saleCost)
|
|
412
|
-
|
|
413
|
-
return {
|
|
414
|
-
id: 'sell-stay',
|
|
415
|
-
type: 'sell-and-stay',
|
|
416
|
-
title: 'Sell & Enter Orbit',
|
|
417
|
-
description: `Sell cargo, remain docked at this location`,
|
|
418
|
-
reason: 'Collect profits now, decide next move later',
|
|
419
|
-
recommended: false,
|
|
420
|
-
highestProfit: false,
|
|
421
|
-
estimatedProfit: saleProfit,
|
|
422
|
-
saleRevenue,
|
|
423
|
-
cargoSale,
|
|
424
|
-
cargoProfitLoss: saleProfit,
|
|
425
|
-
travelTimeBreakdown:
|
|
426
|
-
unloadTime !== undefined
|
|
427
|
-
? {
|
|
428
|
-
unloadTime,
|
|
429
|
-
loadTime: UInt32.zero,
|
|
430
|
-
rechargeTime: UInt32.zero,
|
|
431
|
-
flightTime: UInt32.zero,
|
|
432
|
-
total: unloadTime,
|
|
433
|
-
}
|
|
434
|
-
: undefined,
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Details about a cargo item being sold
|
|
440
|
-
*/
|
|
441
|
-
export interface CargoSaleItem {
|
|
442
|
-
goodId: UInt16
|
|
443
|
-
goodName: string
|
|
444
|
-
quantity: UInt32
|
|
445
|
-
/** Price per unit at sale location */
|
|
446
|
-
pricePerUnit: UInt32
|
|
447
|
-
/** Total revenue from this item */
|
|
448
|
-
revenue: UInt64
|
|
449
|
-
/** Original cost (paid) per unit */
|
|
450
|
-
costPerUnit: UInt64
|
|
451
|
-
/** Profit/loss on this item */
|
|
452
|
-
profit: Int64
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Info about a discounted good for explore options
|
|
457
|
-
*/
|
|
458
|
-
export interface DiscountedItemInfo {
|
|
459
|
-
goodId: number
|
|
460
|
-
name: string
|
|
461
|
-
rarity: string
|
|
462
|
-
discountPercent: number
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* A potential deal available at a destination (for explore options)
|
|
467
|
-
*/
|
|
468
|
-
export interface PotentialDeal {
|
|
469
|
-
goodId: number
|
|
470
|
-
goodName: string
|
|
471
|
-
destinationCoords: Coordinates
|
|
472
|
-
marginPercent: number
|
|
473
|
-
profitPerSecond: number
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Create an "Explore" option (travel to find opportunities)
|
|
478
|
-
*/
|
|
479
|
-
export function createExploreOption(
|
|
480
|
-
destination: Location,
|
|
481
|
-
travelTime?: UInt32,
|
|
482
|
-
discountedGood?: DiscountedItemInfo,
|
|
483
|
-
travelTimeBreakdown?: EstimatedTravelTime,
|
|
484
|
-
potentialDeals?: PotentialDeal[]
|
|
485
|
-
): CollectOption {
|
|
486
|
-
let description = 'Travel to look for trading opportunities'
|
|
487
|
-
let reason = 'No profitable trades found nearby'
|
|
488
|
-
|
|
489
|
-
if (potentialDeals && potentialDeals.length > 0) {
|
|
490
|
-
const bestDeal = potentialDeals[0]
|
|
491
|
-
description = `${potentialDeals.length} deal${
|
|
492
|
-
potentialDeals.length > 1 ? 's' : ''
|
|
493
|
-
} available — best: ${bestDeal.goodName}`
|
|
494
|
-
reason = `${bestDeal.marginPercent.toFixed(0)}% margin, ${bestDeal.profitPerSecond.toFixed(
|
|
495
|
-
1
|
|
496
|
-
)}/s`
|
|
497
|
-
} else if (discountedGood) {
|
|
498
|
-
const {name, discountPercent} = discountedGood
|
|
499
|
-
if (discountPercent >= 60) {
|
|
500
|
-
description = `${name} at ${discountPercent}% off`
|
|
501
|
-
reason = 'Legendary find — extremely rare opportunity'
|
|
502
|
-
} else if (discountPercent >= 40) {
|
|
503
|
-
description = `${name} at ${discountPercent}% off`
|
|
504
|
-
reason = 'Epic deal — exceptional prices'
|
|
505
|
-
} else if (discountPercent >= 23) {
|
|
506
|
-
description = `${name} at ${discountPercent}% off`
|
|
507
|
-
reason = 'Rare discount — well below market'
|
|
508
|
-
} else if (discountPercent >= 8) {
|
|
509
|
-
description = `${name} at ${discountPercent}% off`
|
|
510
|
-
reason = `Good prices on ${name}`
|
|
511
|
-
} else {
|
|
512
|
-
description = `${name} slightly discounted`
|
|
513
|
-
reason = 'Minor savings available'
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
return {
|
|
518
|
-
id: `explore-${destination.coordinates.x}-${destination.coordinates.y}`,
|
|
519
|
-
type: 'explore',
|
|
520
|
-
title: 'Move',
|
|
521
|
-
description,
|
|
522
|
-
reason,
|
|
523
|
-
recommended: false,
|
|
524
|
-
highestProfit: false,
|
|
525
|
-
estimatedProfit: UInt64.zero,
|
|
526
|
-
destination,
|
|
527
|
-
travelTime,
|
|
528
|
-
travelTimeBreakdown,
|
|
529
|
-
discountedGood,
|
|
530
|
-
potentialDeals,
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Callbacks for collect analysis (provided by manager)
|
|
536
|
-
*/
|
|
537
|
-
export interface CollectAnalysisCallbacks {
|
|
538
|
-
getNearbyLocations: (origin: Coordinates, maxDistance: number) => Promise<Location[]>
|
|
539
|
-
getMarketPrices: (location: Coordinates) => Promise<ItemPrice[]>
|
|
540
|
-
getGameSeed?: () => Checksum256Type
|
|
541
|
-
getState?: () => ServerContract.Types.state_row
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Analyze all collect options for a ship that has arrived at its destination.
|
|
546
|
-
* Returns all available options sorted by estimated profit.
|
|
547
|
-
*/
|
|
548
|
-
export async function analyzeCollectOptions(
|
|
549
|
-
ship: Ship,
|
|
550
|
-
arrivedAt: Coordinates,
|
|
551
|
-
callbacks: CollectAnalysisCallbacks,
|
|
552
|
-
options: CollectAnalysisOptions = {}
|
|
553
|
-
): Promise<CollectAnalysis> {
|
|
554
|
-
const {playerBalance = Infinity, minSaleImprovement = 100} = options
|
|
555
|
-
|
|
556
|
-
const cargo = ship.sellableCargo
|
|
557
|
-
const hasCargo = cargo.length > 0
|
|
558
|
-
|
|
559
|
-
const originPrices = await callbacks.getMarketPrices(arrivedAt)
|
|
560
|
-
const priceMap = new Map<number, UInt64>(originPrices.map((p) => [Number(p.id), p.price]))
|
|
561
|
-
|
|
562
|
-
const {revenue: cargoValueHere, cost: cargoCost} = analyzeCargoSale(cargo, priceMap)
|
|
563
|
-
|
|
564
|
-
const cargoSaleHere = buildCargoSaleItems(cargo, priceMap)
|
|
565
|
-
|
|
566
|
-
const collectOptions: CollectOption[] = []
|
|
567
|
-
|
|
568
|
-
const maxDistance = options.maxDistance ?? Number(ship.maxDistance)
|
|
569
|
-
const nearbyLocations = await callbacks.getNearbyLocations(arrivedAt, maxDistance)
|
|
570
|
-
|
|
571
|
-
const dealsAtOrigin = await findDealsForShip(
|
|
572
|
-
ship,
|
|
573
|
-
arrivedAt,
|
|
574
|
-
callbacks.getNearbyLocations,
|
|
575
|
-
callbacks.getMarketPrices,
|
|
576
|
-
{
|
|
577
|
-
maxDeals: 5,
|
|
578
|
-
maxDistance,
|
|
579
|
-
playerBalance: playerBalance + Number(cargoValueHere),
|
|
580
|
-
availableSpace: Number(ship.maxCapacity),
|
|
581
|
-
}
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
if (hasCargo && dealsAtOrigin.length > 0) {
|
|
585
|
-
const cargoGoodIds = new Set(cargo.map((c) => Number(c.item_id)))
|
|
586
|
-
|
|
587
|
-
for (const deal of dealsAtOrigin.slice(0, 3)) {
|
|
588
|
-
const dealGoodId = Number(deal.item.id)
|
|
589
|
-
if (cargoGoodIds.has(dealGoodId)) {
|
|
590
|
-
continue
|
|
591
|
-
}
|
|
592
|
-
const unloadTime = calculateUnloadTime(ship, cargo)
|
|
593
|
-
const option = createSellAndTradeOption(
|
|
594
|
-
cargoValueHere,
|
|
595
|
-
cargoCost,
|
|
596
|
-
deal,
|
|
597
|
-
cargoSaleHere,
|
|
598
|
-
unloadTime
|
|
599
|
-
)
|
|
600
|
-
collectOptions.push(option)
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (hasCargo) {
|
|
605
|
-
const locationsToCheck = nearbyLocations.slice(0, 10)
|
|
606
|
-
const allDestPrices = await Promise.all(
|
|
607
|
-
locationsToCheck.map((loc) => callbacks.getMarketPrices(loc.coordinates))
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
const candidateLocations: Array<{
|
|
611
|
-
destLocation: Location
|
|
612
|
-
destPriceMap: Map<number, UInt64>
|
|
613
|
-
destRevenue: UInt64
|
|
614
|
-
improvement: Int64
|
|
615
|
-
}> = []
|
|
616
|
-
|
|
617
|
-
for (let i = 0; i < locationsToCheck.length; i++) {
|
|
618
|
-
const destLocation = locationsToCheck[i]
|
|
619
|
-
const destPrices = allDestPrices[i]
|
|
620
|
-
const destPriceMap = new Map<number, UInt64>(
|
|
621
|
-
destPrices.map((p) => [Number(p.id), p.price])
|
|
622
|
-
)
|
|
623
|
-
|
|
624
|
-
const {revenue: destRevenue} = analyzeCargoSale(cargo, destPriceMap)
|
|
625
|
-
const improvement = Int64.from(destRevenue).subtracting(cargoValueHere)
|
|
626
|
-
|
|
627
|
-
if (improvement.gt(Int64.from(minSaleImprovement))) {
|
|
628
|
-
candidateLocations.push({destLocation, destPriceMap, destRevenue, improvement})
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const betterSaleResults = await Promise.all(
|
|
633
|
-
candidateLocations.map(
|
|
634
|
-
async ({destLocation, destPriceMap, destRevenue, improvement}) => {
|
|
635
|
-
const distance = distanceBetweenCoordinates(arrivedAt, destLocation.coordinates)
|
|
636
|
-
const needsRecharge = !ship.hasEnergyFor(distance)
|
|
637
|
-
const travelEstimate = estimateTravelTime(
|
|
638
|
-
ship as ShipLike,
|
|
639
|
-
ship.totalMass,
|
|
640
|
-
distance,
|
|
641
|
-
{
|
|
642
|
-
needsRecharge,
|
|
643
|
-
}
|
|
644
|
-
)
|
|
645
|
-
|
|
646
|
-
const dealsAfterSale = await findDealsForShip(
|
|
647
|
-
ship,
|
|
648
|
-
destLocation.coordinates,
|
|
649
|
-
callbacks.getNearbyLocations,
|
|
650
|
-
callbacks.getMarketPrices,
|
|
651
|
-
{
|
|
652
|
-
maxDeals: 1,
|
|
653
|
-
maxDistance,
|
|
654
|
-
playerBalance: destRevenue,
|
|
655
|
-
availableSpace: Number(ship.maxCapacity),
|
|
656
|
-
}
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
return {
|
|
660
|
-
better: {
|
|
661
|
-
location: destLocation,
|
|
662
|
-
price: UInt32.from(
|
|
663
|
-
destRevenue.dividing(
|
|
664
|
-
cargo.reduce((s, c) => s.adding(c.quantity), UInt64.zero)
|
|
665
|
-
)
|
|
666
|
-
),
|
|
667
|
-
revenue: destRevenue,
|
|
668
|
-
improvement,
|
|
669
|
-
bestDealAfterSale: dealsAfterSale[0],
|
|
670
|
-
distance,
|
|
671
|
-
travelTime: travelEstimate.total,
|
|
672
|
-
travelTimeBreakdown: travelEstimate,
|
|
673
|
-
} as BetterSaleLocation,
|
|
674
|
-
destPriceMap,
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
)
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
const betterSaleLocations = betterSaleResults.sort(
|
|
681
|
-
(a, b) => Number(b.better.improvement) - Number(a.better.improvement)
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
for (const {better, destPriceMap} of betterSaleLocations.slice(0, 2)) {
|
|
685
|
-
const option = createTravelToSellOption(
|
|
686
|
-
cargoValueHere,
|
|
687
|
-
cargoCost,
|
|
688
|
-
better,
|
|
689
|
-
cargo,
|
|
690
|
-
destPriceMap
|
|
691
|
-
)
|
|
692
|
-
collectOptions.push(option)
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
if (hasCargo && dealsAtOrigin.length === 0) {
|
|
697
|
-
const locationsToCheck = nearbyLocations.slice(0, 10)
|
|
698
|
-
const allDealsAtDest = await Promise.all(
|
|
699
|
-
locationsToCheck.map((destLocation) =>
|
|
700
|
-
findDealsForShip(
|
|
701
|
-
ship,
|
|
702
|
-
destLocation.coordinates,
|
|
703
|
-
callbacks.getNearbyLocations,
|
|
704
|
-
callbacks.getMarketPrices,
|
|
705
|
-
{
|
|
706
|
-
maxDeals: 1,
|
|
707
|
-
maxDistance,
|
|
708
|
-
playerBalance: UInt64.from(playerBalance).adding(cargoValueHere),
|
|
709
|
-
availableSpace: Number(ship.maxCapacity),
|
|
710
|
-
}
|
|
711
|
-
)
|
|
712
|
-
)
|
|
713
|
-
)
|
|
714
|
-
|
|
715
|
-
const repositionLocations: RepositionLocation[] = []
|
|
716
|
-
for (let i = 0; i < locationsToCheck.length; i++) {
|
|
717
|
-
const destLocation = locationsToCheck[i]
|
|
718
|
-
const dealsAtDest = allDealsAtDest[i]
|
|
719
|
-
|
|
720
|
-
if (dealsAtDest.length > 0) {
|
|
721
|
-
const distance = distanceBetweenCoordinates(arrivedAt, destLocation.coordinates)
|
|
722
|
-
const needsRecharge = !ship.hasEnergyFor(distance)
|
|
723
|
-
const travelEstimate = estimateTravelTime(
|
|
724
|
-
ship as ShipLike,
|
|
725
|
-
ship.totalMass,
|
|
726
|
-
distance,
|
|
727
|
-
{
|
|
728
|
-
needsRecharge,
|
|
729
|
-
unloadMass: calculateCargoMass(cargo),
|
|
730
|
-
}
|
|
731
|
-
)
|
|
732
|
-
|
|
733
|
-
repositionLocations.push({
|
|
734
|
-
location: destLocation,
|
|
735
|
-
bestDeal: dealsAtDest[0],
|
|
736
|
-
distance,
|
|
737
|
-
travelTime: travelEstimate.total,
|
|
738
|
-
travelTimeBreakdown: travelEstimate,
|
|
739
|
-
})
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
repositionLocations.sort((a, b) => b.bestDeal.profitPerSecond - a.bestDeal.profitPerSecond)
|
|
744
|
-
|
|
745
|
-
for (const reposition of repositionLocations.slice(0, 2)) {
|
|
746
|
-
const option = createSellAndRepositionOption(
|
|
747
|
-
cargoValueHere,
|
|
748
|
-
cargoCost,
|
|
749
|
-
reposition,
|
|
750
|
-
cargoSaleHere
|
|
751
|
-
)
|
|
752
|
-
collectOptions.push(option)
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
if (hasCargo) {
|
|
757
|
-
const unloadTime = calculateUnloadTime(ship, cargo)
|
|
758
|
-
const sellAndStay = createSellAndStayOption(
|
|
759
|
-
cargoValueHere,
|
|
760
|
-
cargoCost,
|
|
761
|
-
cargoSaleHere,
|
|
762
|
-
unloadTime
|
|
763
|
-
)
|
|
764
|
-
collectOptions.push(sellAndStay)
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
if (!hasCargo && dealsAtOrigin.length > 0) {
|
|
768
|
-
for (const deal of dealsAtOrigin.slice(0, 3)) {
|
|
769
|
-
const option: CollectOption = {
|
|
770
|
-
id: `trade-${deal.destination.coordinates.x}-${deal.destination.coordinates.y}-${deal.item.id}`,
|
|
771
|
-
type: 'sell-and-trade',
|
|
772
|
-
title: `Trade ${deal.item.item.name}`,
|
|
773
|
-
description: `Buy ${deal.maxQuantity} ${deal.item.item.name}, deliver to (${deal.destination.coordinates.x}, ${deal.destination.coordinates.y})`,
|
|
774
|
-
reason: `${deal.marginPercent.toFixed(0)}% margin, ${deal.profitPerSecond.toFixed(
|
|
775
|
-
1
|
|
776
|
-
)}/s profit rate`,
|
|
777
|
-
recommended: false,
|
|
778
|
-
highestProfit: false,
|
|
779
|
-
estimatedProfit: deal.totalProfit,
|
|
780
|
-
purchaseCost: UInt64.from(deal.buyPrice).multiplying(deal.maxQuantity),
|
|
781
|
-
nextTradeProfit: deal.totalProfit,
|
|
782
|
-
profitPerSecond: deal.profitPerSecond,
|
|
783
|
-
marginPercent: deal.marginPercent,
|
|
784
|
-
destination: deal.destination,
|
|
785
|
-
deal,
|
|
786
|
-
travelTime: deal.travelTime,
|
|
787
|
-
travelTimeBreakdown: {
|
|
788
|
-
unloadTime: UInt32.zero,
|
|
789
|
-
loadTime: deal.travelTimeBreakdown.loadTime,
|
|
790
|
-
rechargeTime: deal.travelTimeBreakdown.rechargeTime,
|
|
791
|
-
flightTime: deal.travelTimeBreakdown.flightTime,
|
|
792
|
-
total: deal.travelTimeBreakdown.total,
|
|
793
|
-
},
|
|
794
|
-
}
|
|
795
|
-
collectOptions.push(option)
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
if (collectOptions.length === 0) {
|
|
800
|
-
const gameSeed = callbacks.getGameSeed?.()
|
|
801
|
-
const state = callbacks.getState?.()
|
|
802
|
-
|
|
803
|
-
interface ExploreCandidate {
|
|
804
|
-
dest: Location
|
|
805
|
-
travelTime: UInt32
|
|
806
|
-
travelTimeBreakdown: EstimatedTravelTime
|
|
807
|
-
discountedGood?: DiscountedItemInfo
|
|
808
|
-
bestDiscount: number
|
|
809
|
-
potentialDeals?: PotentialDeal[]
|
|
810
|
-
score: number
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
const exploreCandidates: ExploreCandidate[] = []
|
|
814
|
-
|
|
815
|
-
for (const dest of nearbyLocations.slice(0, 10)) {
|
|
816
|
-
const distance = distanceBetweenCoordinates(arrivedAt, dest.coordinates)
|
|
817
|
-
const needsRecharge = !ship.hasEnergyFor(distance)
|
|
818
|
-
const unloadMass = hasCargo ? calculateCargoMass(cargo) : UInt32.zero
|
|
819
|
-
const travelEstimate = estimateTravelTime(ship as ShipLike, ship.totalMass, distance, {
|
|
820
|
-
needsRecharge,
|
|
821
|
-
unloadMass,
|
|
822
|
-
})
|
|
823
|
-
|
|
824
|
-
let discountedGood: DiscountedItemInfo | undefined
|
|
825
|
-
let bestDiscount = 0
|
|
826
|
-
|
|
827
|
-
if (gameSeed && state) {
|
|
828
|
-
const allGoods = getItems()
|
|
829
|
-
for (const good of allGoods) {
|
|
830
|
-
const rarity = getRarity(gameSeed, state.seed, dest.coordinates, good.id)
|
|
831
|
-
if (rarity.minMultiplier < 1.0) {
|
|
832
|
-
const discountPercent = Math.round((1 - rarity.minMultiplier) * 100)
|
|
833
|
-
if (discountPercent > bestDiscount) {
|
|
834
|
-
bestDiscount = discountPercent
|
|
835
|
-
const rarityName =
|
|
836
|
-
rarity.rarity === Rarities.legendary
|
|
837
|
-
? 'Legendary'
|
|
838
|
-
: rarity.rarity === Rarities.epic
|
|
839
|
-
? 'Epic'
|
|
840
|
-
: rarity.rarity === Rarities.rare
|
|
841
|
-
? 'Rare'
|
|
842
|
-
: rarity.rarity === Rarities.uncommon
|
|
843
|
-
? 'Uncommon'
|
|
844
|
-
: 'Common'
|
|
845
|
-
discountedGood = {
|
|
846
|
-
goodId: Number(good.id),
|
|
847
|
-
name: good.name,
|
|
848
|
-
rarity: rarityName,
|
|
849
|
-
discountPercent,
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
const destDeals = await findDealsForShip(
|
|
857
|
-
ship,
|
|
858
|
-
dest.coordinates,
|
|
859
|
-
callbacks.getNearbyLocations,
|
|
860
|
-
callbacks.getMarketPrices,
|
|
861
|
-
{maxDeals: 2}
|
|
862
|
-
)
|
|
863
|
-
|
|
864
|
-
const potentialDeals: PotentialDeal[] = destDeals.map((d) => ({
|
|
865
|
-
goodId: Number(d.item.id),
|
|
866
|
-
goodName: d.item.item.name,
|
|
867
|
-
destinationCoords: d.destination.coordinates,
|
|
868
|
-
marginPercent: d.marginPercent,
|
|
869
|
-
profitPerSecond: d.profitPerSecond,
|
|
870
|
-
}))
|
|
871
|
-
|
|
872
|
-
let score = 0
|
|
873
|
-
if (potentialDeals.length > 0) {
|
|
874
|
-
score = potentialDeals[0].profitPerSecond
|
|
875
|
-
} else if (bestDiscount > 0) {
|
|
876
|
-
score = bestDiscount * 0.01
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
exploreCandidates.push({
|
|
880
|
-
dest,
|
|
881
|
-
travelTime: travelEstimate.total,
|
|
882
|
-
travelTimeBreakdown: travelEstimate,
|
|
883
|
-
discountedGood,
|
|
884
|
-
bestDiscount,
|
|
885
|
-
potentialDeals: potentialDeals.length > 0 ? potentialDeals : undefined,
|
|
886
|
-
score,
|
|
887
|
-
})
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
exploreCandidates.sort((a, b) => b.score - a.score)
|
|
891
|
-
|
|
892
|
-
for (const candidate of exploreCandidates.slice(0, 3)) {
|
|
893
|
-
const option = createExploreOption(
|
|
894
|
-
candidate.dest,
|
|
895
|
-
candidate.travelTime,
|
|
896
|
-
candidate.discountedGood,
|
|
897
|
-
candidate.travelTimeBreakdown,
|
|
898
|
-
candidate.potentialDeals
|
|
899
|
-
)
|
|
900
|
-
collectOptions.push(option)
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
const orbitOption = createOrbitOption()
|
|
905
|
-
collectOptions.push(orbitOption)
|
|
906
|
-
|
|
907
|
-
const MIN_MARGIN_THRESHOLD = 15
|
|
908
|
-
const MIN_PROFIT_PER_SECOND_THRESHOLD = 0.5
|
|
909
|
-
|
|
910
|
-
collectOptions.sort((a, b) => (b.profitPerSecond ?? 0) - (a.profitPerSecond ?? 0))
|
|
911
|
-
|
|
912
|
-
if (collectOptions.length > 0) {
|
|
913
|
-
const bestByProfitPerSecond = collectOptions[0]
|
|
914
|
-
const meetsQualityThreshold =
|
|
915
|
-
(bestByProfitPerSecond.marginPercent ?? 0) > MIN_MARGIN_THRESHOLD ||
|
|
916
|
-
(bestByProfitPerSecond.profitPerSecond ?? 0) > MIN_PROFIT_PER_SECOND_THRESHOLD
|
|
917
|
-
|
|
918
|
-
if (meetsQualityThreshold) {
|
|
919
|
-
bestByProfitPerSecond.recommended = true
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const bestByProfit = collectOptions.reduce((best, opt) =>
|
|
923
|
-
opt.estimatedProfit > best.estimatedProfit ? opt : best
|
|
924
|
-
)
|
|
925
|
-
if (bestByProfit.id !== bestByProfitPerSecond.id || !meetsQualityThreshold) {
|
|
926
|
-
bestByProfit.highestProfit = true
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
return {
|
|
931
|
-
arrivedAt,
|
|
932
|
-
ship,
|
|
933
|
-
cargo,
|
|
934
|
-
cargoValueHere,
|
|
935
|
-
options: collectOptions,
|
|
936
|
-
hasProfitableOptions: collectOptions.some((o) => o.estimatedProfit.gt(UInt64.zero)),
|
|
937
|
-
}
|
|
938
|
-
}
|