@shipload/sdk 2.0.0-rc1 → 2.0.0-rc11

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