@shipload/sdk 2.0.0-rc2 → 2.0.0-rc21

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 (84) hide show
  1. package/README.md +1 -349
  2. package/lib/shipload.d.ts +1729 -1127
  3. package/lib/shipload.js +7944 -3165
  4. package/lib/shipload.js.map +1 -1
  5. package/lib/shipload.m.js +7487 -2840
  6. package/lib/shipload.m.js.map +1 -1
  7. package/package.json +6 -4
  8. package/src/capabilities/crafting.ts +22 -0
  9. package/src/capabilities/gathering.ts +36 -0
  10. package/src/capabilities/guards.ts +3 -8
  11. package/src/capabilities/hauling.ts +22 -0
  12. package/src/capabilities/index.ts +4 -1
  13. package/src/capabilities/modules.ts +86 -0
  14. package/src/capabilities/storage.ts +101 -9
  15. package/src/contracts/server.ts +785 -293
  16. package/src/data/capabilities.ts +408 -0
  17. package/src/data/categories.ts +55 -0
  18. package/src/data/colors.ts +71 -0
  19. package/src/data/entities.json +50 -0
  20. package/src/data/item-ids.ts +75 -0
  21. package/src/data/items.json +252 -0
  22. package/src/data/locations.ts +53 -0
  23. package/src/data/metadata.ts +208 -0
  24. package/src/data/nebula-adjectives.json +211 -0
  25. package/src/data/nebula-nouns.json +151 -0
  26. package/src/data/recipes-runtime.ts +65 -0
  27. package/src/data/recipes.json +878 -0
  28. package/src/data/syllables.json +1386 -780
  29. package/src/data/tiers.ts +45 -0
  30. package/src/derivation/crafting.ts +348 -0
  31. package/src/derivation/index.ts +30 -0
  32. package/src/derivation/location-size.ts +15 -0
  33. package/src/derivation/resources.ts +112 -0
  34. package/src/derivation/stats.ts +146 -0
  35. package/src/derivation/stratum.ts +134 -0
  36. package/src/derivation/tiers.ts +54 -0
  37. package/src/entities/cargo-utils.ts +10 -68
  38. package/src/entities/container.ts +37 -0
  39. package/src/entities/entity-inventory.ts +13 -13
  40. package/src/entities/inventory-accessor.ts +2 -6
  41. package/src/entities/location.ts +5 -200
  42. package/src/entities/makers.ts +144 -17
  43. package/src/entities/player.ts +1 -274
  44. package/src/entities/ship-deploy.ts +258 -0
  45. package/src/entities/ship.ts +28 -34
  46. package/src/entities/warehouse.ts +35 -7
  47. package/src/errors.ts +59 -5
  48. package/src/format.ts +12 -0
  49. package/src/index-module.ts +188 -50
  50. package/src/managers/actions.ts +138 -88
  51. package/src/managers/context.ts +19 -9
  52. package/src/managers/index.ts +0 -1
  53. package/src/managers/locations.ts +2 -85
  54. package/src/market/items.ts +41 -0
  55. package/src/nft/description.ts +176 -0
  56. package/src/nft/deserializers.ts +83 -0
  57. package/src/nft/index.ts +2 -0
  58. package/src/resolution/describe-module.ts +165 -0
  59. package/src/resolution/display-name.ts +43 -0
  60. package/src/resolution/resolve-item.ts +358 -0
  61. package/src/scheduling/projection.ts +200 -67
  62. package/src/scheduling/schedule.ts +2 -2
  63. package/src/shipload.ts +10 -5
  64. package/src/subscriptions/connection.ts +154 -0
  65. package/src/subscriptions/debug.ts +17 -0
  66. package/src/subscriptions/index.ts +5 -0
  67. package/src/subscriptions/manager.ts +240 -0
  68. package/src/subscriptions/mappers.ts +28 -0
  69. package/src/subscriptions/types.ts +143 -0
  70. package/src/travel/travel.ts +37 -23
  71. package/src/types/capabilities.ts +11 -14
  72. package/src/types/entity-traits.ts +3 -4
  73. package/src/types/entity.ts +9 -6
  74. package/src/types.ts +72 -72
  75. package/src/utils/system.ts +66 -53
  76. package/src/capabilities/extraction.ts +0 -37
  77. package/src/data/goods.json +0 -23
  78. package/src/managers/trades.ts +0 -119
  79. package/src/market/goods.ts +0 -31
  80. package/src/market/market.ts +0 -208
  81. package/src/market/rolls.ts +0 -8
  82. package/src/trading/collect.ts +0 -938
  83. package/src/trading/deal.ts +0 -207
  84. package/src/trading/trade.ts +0 -203
@@ -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, 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
- } from '../travel/travel'
14
- import {getGood, getGoods} from '../market/goods'
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 = getGood(c.good_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?: DiscountedGoodInfo
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.good_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.good_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.good_id,
231
- goodName: c.good?.name ?? `Good #${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.good.id}`,
275
- type: 'sell-and-trade',
276
- title: `Trade ${deal.good.good.name}`,
277
- description: `Sell cargo, buy ${deal.maxQuantity} ${deal.good.good.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.good.good.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 DiscountedGoodInfo {
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?: DiscountedGoodInfo,
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<GoodPrice[]>
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.good_id)))
586
-
587
- for (const deal of dealsAtOrigin.slice(0, 3)) {
588
- const dealGoodId = Number(deal.good.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.good.id}`,
771
- type: 'sell-and-trade',
772
- title: `Trade ${deal.good.good.name}`,
773
- description: `Buy ${deal.maxQuantity} ${deal.good.good.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?: DiscountedGoodInfo
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: DiscountedGoodInfo | undefined
825
- let bestDiscount = 0
826
-
827
- if (gameSeed && state) {
828
- const allGoods = getGoods()
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.good.id),
866
- goodName: d.good.good.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
- }