@shipload/sdk 1.0.0-next.6 → 1.0.0-next.61

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 (156) hide show
  1. package/lib/scan.d.ts +52 -0
  2. package/lib/scan.js +162 -0
  3. package/lib/scan.js.map +1 -0
  4. package/lib/scan.m.js +152 -0
  5. package/lib/scan.m.js.map +1 -0
  6. package/lib/shipload.d.ts +3601 -1364
  7. package/lib/shipload.js +15671 -5261
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +15392 -5209
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1173 -0
  12. package/lib/testing.js +4971 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +4965 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.test.ts +82 -0
  18. package/src/capabilities/craftable.ts +82 -0
  19. package/src/capabilities/crafting.test.ts +35 -0
  20. package/src/capabilities/crafting.ts +34 -7
  21. package/src/capabilities/gathering.test.ts +29 -0
  22. package/src/capabilities/gathering.ts +37 -19
  23. package/src/capabilities/hauling.ts +0 -5
  24. package/src/capabilities/index.ts +0 -1
  25. package/src/capabilities/modules.ts +27 -30
  26. package/src/capabilities/movement.ts +1 -1
  27. package/src/capabilities/storage.ts +16 -1
  28. package/src/contracts/platform.ts +231 -3
  29. package/src/contracts/server.ts +1518 -501
  30. package/src/coordinates/address.ts +88 -0
  31. package/src/coordinates/constants.test.ts +15 -0
  32. package/src/coordinates/constants.ts +23 -0
  33. package/src/coordinates/index.ts +15 -0
  34. package/src/coordinates/memo.test.ts +47 -0
  35. package/src/coordinates/memo.ts +20 -0
  36. package/src/coordinates/permutation.ts +77 -0
  37. package/src/coordinates/regions.ts +48 -0
  38. package/src/coordinates/sectors.ts +115 -0
  39. package/src/data/capabilities.ts +49 -18
  40. package/src/data/capability-formulas.ts +26 -14
  41. package/src/data/catalog.ts +7 -8
  42. package/src/data/colors.ts +14 -48
  43. package/src/data/entities.json +380 -15
  44. package/src/data/item-ids.ts +51 -13
  45. package/src/data/items.json +318 -77
  46. package/src/data/kind-registry.json +195 -0
  47. package/src/data/kind-registry.ts +182 -0
  48. package/src/data/metadata.ts +291 -51
  49. package/src/data/recipes-runtime.ts +6 -23
  50. package/src/data/recipes.json +2260 -173
  51. package/src/data/tiers.ts +9 -6
  52. package/src/derivation/build-methods.test.ts +13 -0
  53. package/src/derivation/build-methods.ts +48 -0
  54. package/src/derivation/capabilities.test.ts +348 -0
  55. package/src/derivation/capabilities.ts +693 -0
  56. package/src/derivation/capability-mappings.ts +60 -16
  57. package/src/derivation/crafting.test.ts +17 -0
  58. package/src/derivation/crafting.ts +57 -34
  59. package/src/derivation/index.ts +27 -2
  60. package/src/derivation/recipe-usage.test.ts +90 -0
  61. package/src/derivation/recipe-usage.ts +141 -0
  62. package/src/derivation/reserve-regen.ts +34 -0
  63. package/src/derivation/resources.ts +9 -1
  64. package/src/derivation/rollups.test.ts +55 -0
  65. package/src/derivation/rollups.ts +72 -0
  66. package/src/derivation/stars.test.ts +67 -0
  67. package/src/derivation/stars.ts +25 -0
  68. package/src/derivation/stat-scaling.ts +12 -0
  69. package/src/derivation/stats.ts +6 -6
  70. package/src/derivation/stratum.ts +26 -22
  71. package/src/derivation/tiers.ts +40 -7
  72. package/src/derivation/upgrades.ts +14 -0
  73. package/src/derivation/wormhole.ts +154 -0
  74. package/src/entities/entity.ts +102 -0
  75. package/src/entities/gamestate.ts +3 -28
  76. package/src/entities/makers.ts +141 -135
  77. package/src/entities/slot-multiplier.ts +43 -0
  78. package/src/errors.ts +12 -16
  79. package/src/format.ts +26 -4
  80. package/src/index-module.ts +333 -35
  81. package/src/managers/actions.ts +668 -103
  82. package/src/managers/base.ts +6 -2
  83. package/src/managers/cluster.test.ts +39 -0
  84. package/src/managers/cluster.ts +53 -0
  85. package/src/managers/construction-types.ts +80 -0
  86. package/src/managers/construction.ts +443 -0
  87. package/src/managers/context.ts +29 -1
  88. package/src/managers/coordinates.ts +14 -0
  89. package/src/managers/entities.ts +27 -66
  90. package/src/managers/epochs.ts +40 -0
  91. package/src/managers/index.ts +20 -1
  92. package/src/managers/locations.ts +25 -29
  93. package/src/managers/nft.test.ts +14 -0
  94. package/src/managers/nft.ts +70 -0
  95. package/src/managers/players.ts +25 -0
  96. package/src/managers/plot.ts +122 -0
  97. package/src/nft/atomicassets.abi.json +1342 -0
  98. package/src/nft/atomicassets.ts +237 -0
  99. package/src/nft/atomicdata.ts +130 -0
  100. package/src/nft/buildImmutableData.ts +388 -0
  101. package/src/nft/description.ts +215 -99
  102. package/src/nft/index.ts +3 -0
  103. package/src/planner/index.ts +242 -0
  104. package/src/planner/planner.test.ts +334 -0
  105. package/src/resolution/describe-module.ts +43 -25
  106. package/src/resolution/display-name.ts +38 -10
  107. package/src/resolution/resolve-item.test.ts +48 -0
  108. package/src/resolution/resolve-item.ts +112 -58
  109. package/src/scan/index.ts +244 -0
  110. package/src/scan/scan-wasm.base64.ts +2 -0
  111. package/src/scheduling/accessor.ts +65 -23
  112. package/src/scheduling/availability.test.ts +204 -0
  113. package/src/scheduling/availability.ts +211 -0
  114. package/src/scheduling/cancel.test.ts +542 -0
  115. package/src/scheduling/cancel.ts +254 -0
  116. package/src/scheduling/cluster-stock.test.ts +31 -0
  117. package/src/scheduling/cluster-stock.ts +15 -0
  118. package/src/scheduling/energy.ts +47 -0
  119. package/src/scheduling/idle-resolve.ts +45 -0
  120. package/src/scheduling/jobs.ts +71 -0
  121. package/src/scheduling/lane-core.ts +128 -0
  122. package/src/scheduling/lanes.test.ts +250 -0
  123. package/src/scheduling/lanes.ts +227 -0
  124. package/src/scheduling/projection.ts +257 -133
  125. package/src/scheduling/schedule.test.ts +119 -0
  126. package/src/scheduling/schedule.ts +257 -117
  127. package/src/scheduling/task-cargo.test.ts +44 -0
  128. package/src/scheduling/task-cargo.ts +46 -0
  129. package/src/scheduling/unwrap.test.ts +86 -0
  130. package/src/scheduling/unwrap.ts +190 -0
  131. package/src/shipload.ts +26 -1
  132. package/src/subscriptions/manager.cluster.test.ts +51 -0
  133. package/src/subscriptions/manager.ts +245 -172
  134. package/src/subscriptions/mappers.ts +5 -8
  135. package/src/subscriptions/types.ts +28 -3
  136. package/src/testing/catalog-hash.ts +19 -0
  137. package/src/testing/index.ts +2 -0
  138. package/src/testing/projection-parity.ts +167 -0
  139. package/src/travel/reach.ts +21 -0
  140. package/src/travel/route-planner.ts +288 -0
  141. package/src/travel/route-simulator.ts +170 -0
  142. package/src/travel/travel.ts +188 -122
  143. package/src/types/capabilities.ts +37 -8
  144. package/src/types/entity.ts +3 -3
  145. package/src/types/index.ts +0 -1
  146. package/src/types.ts +51 -16
  147. package/src/utils/cargo.test.ts +14 -0
  148. package/src/utils/cargo.ts +29 -0
  149. package/src/utils/display-name.ts +70 -0
  150. package/src/utils/system.ts +15 -16
  151. package/src/capabilities/loading.ts +0 -8
  152. package/src/entities/container.ts +0 -123
  153. package/src/entities/ship-deploy.ts +0 -295
  154. package/src/entities/ship.ts +0 -221
  155. package/src/entities/warehouse.ts +0 -127
  156. package/src/types/entity-traits.ts +0 -69
@@ -1,106 +1,204 @@
1
1
  import {
2
2
  getModuleCapabilityType,
3
+ MODULE_BUILDER,
3
4
  MODULE_CRAFTER,
4
5
  MODULE_ENGINE,
5
6
  MODULE_GATHERER,
6
7
  MODULE_GENERATOR,
8
+ MODULE_BATTERY,
7
9
  MODULE_HAULER,
8
10
  MODULE_LOADER,
9
11
  MODULE_STORAGE,
10
12
  MODULE_WARP,
11
13
  } from '../capabilities/modules'
12
- import {
13
- ITEM_CONTAINER_T1_PACKED,
14
- ITEM_CONTAINER_T2_PACKED,
15
- ITEM_CRAFTER_T1,
16
- ITEM_ENGINE_T1,
17
- ITEM_GATHERER_T1,
18
- ITEM_GENERATOR_T1,
19
- ITEM_HAULER_T1,
20
- ITEM_LOADER_T1,
21
- ITEM_SHIP_T1_PACKED,
22
- ITEM_STORAGE_T1,
23
- ITEM_WAREHOUSE_T1_PACKED,
24
- ITEM_WARP_T1,
25
- } from '../data/item-ids'
14
+ import {getKindMeta, getTemplateMeta} from '../data/kind-registry'
26
15
  import {decodeStat} from '../derivation/crafting'
27
- import {gathererDepthForTier} from '../entities/ship-deploy'
28
- import {getItem} from '../data/catalog'
16
+ import {
17
+ gathererDepthForTier,
18
+ computeGathererYield,
19
+ moduleTierPct,
20
+ ENGINE_THRUST_TIER_PCT,
21
+ GENERATOR_CAPACITY_TIER_PCT,
22
+ GENERATOR_RECHARGE_TIER_PCT,
23
+ CRAFTER_SPEED_TIER_PCT,
24
+ BUILDER_SPEED_TIER_PCT,
25
+ WARP_RANGE_TIER_PCT,
26
+ LOADER_THRUST_TIER_PCT,
27
+ CARGO_BAY_CAPACITY_TIER_PCT,
28
+ BATTERY_CAPACITY_TIER_PCT,
29
+ } from '../derivation/capabilities'
30
+ import {getItem, tryGetItem} from '../data/catalog'
31
+ import type {ModuleType} from '../types'
32
+ import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
33
+ import {getBaseHullmassFor} from '../derivation/capabilities'
34
+ import {computeEffectiveModuleStat} from '../derivation/stat-scaling'
29
35
 
30
36
  function idiv(a: number, b: number): number {
31
37
  return Math.floor(a / b)
32
38
  }
33
39
 
34
- export function computeBaseHullmass(stats: bigint): number {
40
+ export function toWholeEnergy(milli: number): number {
41
+ return idiv(milli + 500, 1000)
42
+ }
43
+
44
+ export function formatMassTonnes(kg: number): string {
45
+ const tenths = idiv(kg + 50, 100)
46
+ return `${idiv(tenths, 10)}.${tenths % 10} t`
47
+ }
48
+
49
+ function isShipHull(itemId: number): boolean {
50
+ return getPackedEntityType(itemId)?.equals(ENTITY_SHIP) ?? false
51
+ }
52
+
53
+ function sumPackedShipChannels(stats: bigint): number {
54
+ let sum = 0
55
+ for (let slot = 0; slot < 5; slot++) sum += decodeStat(stats, slot)
56
+ return sum
57
+ }
58
+
59
+ export function computeBaseHullmass(itemId: number, stats: bigint): number {
60
+ if (isShipHull(itemId)) {
61
+ return Math.floor(
62
+ (getBaseHullmassFor(itemId) * (10_000 - sumPackedShipChannels(stats))) / 10_000
63
+ )
64
+ }
35
65
  const density = decodeStat(stats, 1)
36
- return 25000 + 75 * density
66
+ return Math.floor((getBaseHullmassFor(itemId) * (2000 - density)) / 2000)
37
67
  }
38
68
 
39
69
  export function computeBaseCapacityShip(stats: bigint): number {
40
- const s = decodeStat(stats, 0) + decodeStat(stats, 2) + decodeStat(stats, 3)
41
- return Math.floor(1_000_000 * 10 ** (s / 2997))
70
+ return Math.floor(5_000_000 * 6 ** (sumPackedShipChannels(stats) / 4995))
71
+ }
72
+
73
+ export function computeBaseCapacityContainer(stats: bigint): number {
74
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
75
+ return Math.floor(22_000_000 * 6 ** (s / 1998))
42
76
  }
43
77
 
44
78
  export function computeBaseCapacityWarehouse(stats: bigint): number {
45
- const s = decodeStat(stats, 0) + decodeStat(stats, 2) + decodeStat(stats, 3)
46
- return Math.floor(20_000_000 * 10 ** (s / 2997))
79
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
80
+ return Math.floor(100_000_000 * 6 ** (s / 1998))
47
81
  }
48
82
 
49
- export const computeEngineThrust = (vol: number): number => 400 + idiv(vol * 3, 4)
50
- export const computeEngineDrain = (thm: number): number => Math.max(30, 50 - idiv(thm, 70))
51
- export const computeGeneratorCap = (com: number): number => 300 + idiv(com, 6)
52
- export const computeGeneratorRech = (fin: number): number => 1 + idiv(fin * 3, 1000)
53
- export const computeGathererYield = (str: number): number => 200 + str
83
+ export function computeBaseCapacityWorkshop(stats: bigint): number {
84
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
85
+ return Math.floor(5_000_000 * 6 ** (s / 1998))
86
+ }
87
+
88
+ const CAPACITY_FN_BY_KIND: Record<string, (stats: bigint) => number> = {
89
+ ship: computeBaseCapacityShip,
90
+ warehouse: computeBaseCapacityWarehouse,
91
+ workshop: computeBaseCapacityWorkshop,
92
+ extractor: computeBaseCapacityContainer,
93
+ factory: computeBaseCapacityContainer,
94
+ builddock: computeBaseCapacityContainer,
95
+ mdriver: computeBaseCapacityContainer,
96
+ mcatcher: computeBaseCapacityContainer,
97
+ container: computeBaseCapacityContainer,
98
+ }
99
+
100
+ export function computeBaseCapacityForEntity(itemId: number, stats: bigint): number {
101
+ const kind = getTemplateMeta(itemId)?.kind
102
+ if (!kind) return 0
103
+ return CAPACITY_FN_BY_KIND[kind.toString()]?.(stats) ?? 0
104
+ }
105
+
106
+ export const computeEngineThrust = (vol: number, tier: number): number =>
107
+ idiv((400 + idiv(vol * 3, 4)) * moduleTierPct(ENGINE_THRUST_TIER_PCT, tier), 100)
108
+ export const computeEngineDrain = (thm: number): number => 2 * Math.max(30, 50 - idiv(thm, 70))
109
+ export const ENGINE_DRAIN_BASE = 156
110
+ export const ENGINE_DRAIN_REF_THRUST = 775
111
+ export const ENGINE_DRAIN_REF_THM = 500
112
+
113
+ export const computeTravelDrain = (totalThrust: number, avgThm: number): number => {
114
+ if (totalThrust <= 0) return 0
115
+ const thermalFactor = computeEngineDrain(avgThm) / computeEngineDrain(ENGINE_DRAIN_REF_THM)
116
+ const thrustFactor = Math.sqrt(ENGINE_DRAIN_REF_THRUST / totalThrust)
117
+ return Math.floor(ENGINE_DRAIN_BASE * 1000 * thermalFactor * thrustFactor)
118
+ }
119
+ export const computeGeneratorCap = (com: number, tier: number): number =>
120
+ idiv((1_300_000 + com * 500) * moduleTierPct(GENERATOR_CAPACITY_TIER_PCT, tier), 100)
121
+ export const computeGeneratorRech = (fin: number, tier: number): number =>
122
+ idiv((2000 + fin * 6) * moduleTierPct(GENERATOR_RECHARGE_TIER_PCT, tier), 100)
54
123
  export const computeGathererDrain = (con: number): number =>
55
- Math.max(250, 1250 - idiv(con * 25, 20))
124
+ 2 * Math.max(250_000, 1_250_000 - con * 1250)
56
125
  export const computeGathererDepth = (tol: number, tier: number): number =>
57
126
  gathererDepthForTier(tol, tier)
58
- export const computeGathererSpeed = (ref: number): number => 100 + idiv(ref * 4, 5)
59
127
  export const computeLoaderMass = (ins: number): number => Math.max(200, 2000 - ins * 2)
60
- export const computeLoaderThrust = (pla: number): number => 1 + idiv(pla, 500)
61
- export const computeCrafterSpeed = (rea: number): number => 100 + idiv(rea * 4, 5)
62
- export const computeCrafterDrain = (fin: number): number => Math.max(5, 30 - idiv(fin, 33))
63
- export const computeHaulerCapacity = (fin: number): number => Math.max(1, 1 + idiv(fin, 400))
128
+ export const computeLoaderThrust = (pla: number, tier: number): number =>
129
+ idiv((1 + idiv(pla * pla, 10000)) * moduleTierPct(LOADER_THRUST_TIER_PCT, tier), 100)
130
+ export const computeCrafterSpeed = (rea: number, tier: number): number =>
131
+ idiv((100 + idiv(rea * 4, 5)) * moduleTierPct(CRAFTER_SPEED_TIER_PCT, tier), 100)
132
+ export const computeCrafterDrain = (fin: number): number =>
133
+ Math.max(5000, 30000 - idiv(fin * 1000, 33))
134
+ export const computeBuilderSpeed = (resonance: number, tier: number): number =>
135
+ idiv((100 + idiv(resonance * 4, 5)) * moduleTierPct(BUILDER_SPEED_TIER_PCT, tier), 100)
136
+ export const computeBuilderDrain = (fineness: number): number =>
137
+ Math.max(5000, 30000 - idiv(fineness * 1000, 33))
138
+ export const computeHaulerCapacity = (fin: number, tier: number): number =>
139
+ Math.max(tier, tier + idiv(fin, 400))
64
140
  export const computeHaulerEfficiency = (con: number): number => 2000 + con * 6
65
- export const computeHaulerDrain = (com: number): number => Math.max(3, 15 - idiv(com, 80))
66
- export const computeWarpRange = (stat: number): number => 100 + stat * 3
141
+ export const supportDrainTierPercent = (tier: number): number => {
142
+ const clampedTier = Math.min(10, Math.max(1, Math.trunc(tier)))
143
+ return Math.max(50, 110 - clampedTier * 10)
144
+ }
145
+ const computeT1LogisticsDrain = (stat: number): number =>
146
+ Math.max(3000, 15000 - Math.min(12000, idiv(stat * 1000, 80)))
147
+ export const computeHaulerDrain = (conductivity: number, tier: number): number =>
148
+ idiv(computeT1LogisticsDrain(conductivity) * supportDrainTierPercent(tier), 100)
149
+ export const computeCargoBayDrain = (cohesion: number, tier: number): number =>
150
+ idiv(computeT1LogisticsDrain(cohesion) * 3 * supportDrainTierPercent(tier), 400)
151
+ export const computeWarpRange = (stat: number, tier: number): number =>
152
+ idiv((100 + stat * 3) * moduleTierPct(WARP_RANGE_TIER_PCT, tier), 100)
153
+ export const computeCargoBayCapacity = (
154
+ strength: number,
155
+ density: number,
156
+ hardness: number,
157
+ tier: number
158
+ ): number =>
159
+ idiv(
160
+ (10_000_000 + idiv((strength + density + hardness) * 50_000_000, 2997)) *
161
+ moduleTierPct(CARGO_BAY_CAPACITY_TIER_PCT, tier),
162
+ 100
163
+ )
164
+ export const computeBatteryBankCapacity = (
165
+ volatility: number,
166
+ thermal: number,
167
+ plasticity: number,
168
+ insulation: number,
169
+ tier: number
170
+ ): number =>
171
+ idiv(
172
+ (2_500_000 + idiv((volatility + thermal + plasticity + insulation) * 7_500_000, 3996)) *
173
+ moduleTierPct(BATTERY_CAPACITY_TIER_PCT, tier),
174
+ 100
175
+ )
67
176
 
68
177
  export function entityDisplayName(itemId: number): string {
69
- switch (itemId) {
70
- case ITEM_SHIP_T1_PACKED:
71
- return 'Ship'
72
- case ITEM_WAREHOUSE_T1_PACKED:
73
- return 'Warehouse'
74
- case ITEM_CONTAINER_T1_PACKED:
75
- return 'Container'
76
- case ITEM_CONTAINER_T2_PACKED:
77
- return 'Container'
78
- default:
79
- return 'Entity'
80
- }
178
+ const template = getTemplateMeta(itemId)
179
+ if (!template) return 'Entity'
180
+ if (template.displayLabel) return template.displayLabel
181
+ return getKindMeta(template.kind)?.defaultLabel || 'Entity'
182
+ }
183
+
184
+ const MODULE_DISPLAY_NAME_BY_TYPE: Partial<Record<ModuleType, string>> = {
185
+ engine: 'Engine',
186
+ generator: 'Power Core',
187
+ gatherer: 'Limpet Bay',
188
+ loader: 'Shuttle Bay',
189
+ crafter: 'Fabricator',
190
+ storage: 'Cargo Hold',
191
+ hauler: 'Tractor Beam',
192
+ warp: 'Warp Drive',
193
+ battery: 'Battery Bank',
194
+ launcher: 'Drive Coil',
195
+ builder: 'Assembly Arm',
81
196
  }
82
197
 
83
198
  export function moduleDisplayName(itemId: number): string {
84
- switch (itemId) {
85
- case ITEM_ENGINE_T1:
86
- return 'Engine'
87
- case ITEM_GENERATOR_T1:
88
- return 'Generator'
89
- case ITEM_GATHERER_T1:
90
- return 'Gatherer'
91
- case ITEM_LOADER_T1:
92
- return 'Loader'
93
- case ITEM_CRAFTER_T1:
94
- return 'Crafter'
95
- case ITEM_STORAGE_T1:
96
- return 'Storage'
97
- case ITEM_HAULER_T1:
98
- return 'Hauler'
99
- case ITEM_WARP_T1:
100
- return 'Warp'
101
- default:
102
- return 'Module'
103
- }
199
+ const item = tryGetItem(itemId)
200
+ if (item?.type !== 'module' || !item.moduleType) return 'Module'
201
+ return MODULE_DISPLAY_NAME_BY_TYPE[item.moduleType] ?? 'Module'
104
202
  }
105
203
 
106
204
  export function formatModuleLine(slot: number, itemId: number, stats: bigint): string {
@@ -115,60 +213,83 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
115
213
 
116
214
  switch (subtype) {
117
215
  case MODULE_ENGINE: {
118
- const vol = decodeStat(stats, 0)
119
- const thm = decodeStat(stats, 1)
120
- out += ` Thrust ${computeEngineThrust(vol)} Drain ${computeEngineDrain(thm)}`
216
+ const vol = computeEffectiveModuleStat(decodeStat(stats, 0))
217
+ const thm = computeEffectiveModuleStat(decodeStat(stats, 1))
218
+ const tier = getItem(itemId).tier
219
+ out += ` Thrust ${computeEngineThrust(vol, tier)} Drain ${computeEngineDrain(thm)}`
121
220
  break
122
221
  }
123
222
  case MODULE_GENERATOR: {
124
- const res = decodeStat(stats, 0)
125
- const ref = decodeStat(stats, 1)
126
- out += ` Capacity ${computeGeneratorCap(res)} Recharge ${computeGeneratorRech(ref)}`
223
+ const res = computeEffectiveModuleStat(decodeStat(stats, 0))
224
+ const ref = computeEffectiveModuleStat(decodeStat(stats, 1))
225
+ const tier = getItem(itemId).tier
226
+ out += ` Capacity ${toWholeEnergy(computeGeneratorCap(res, tier))} Recharge ${toWholeEnergy(
227
+ computeGeneratorRech(ref, tier)
228
+ )}`
127
229
  break
128
230
  }
129
231
  case MODULE_GATHERER: {
130
232
  const str = decodeStat(stats, 0)
131
233
  const tol = decodeStat(stats, 1)
132
- const con = decodeStat(stats, 3)
133
- const ref = decodeStat(stats, 4)
234
+ const con = decodeStat(stats, 2)
134
235
  const tier = getItem(itemId).tier
135
- out += ` Yield ${computeGathererYield(str)} Depth ${computeGathererDepth(
236
+ out += ` Yield ${computeGathererYield(str, tier)} Depth ${computeGathererDepth(
136
237
  tol,
137
238
  tier
138
- )} Speed ${computeGathererSpeed(ref)} Drain ${computeGathererDrain(con)}`
239
+ )} Drain ${toWholeEnergy(computeGathererDrain(con))}`
139
240
  break
140
241
  }
141
242
  case MODULE_LOADER: {
142
243
  const fin = decodeStat(stats, 0)
143
244
  const pla = decodeStat(stats, 1)
144
- out += ` Mass ${computeLoaderMass(fin)} Thrust ${computeLoaderThrust(pla)}`
245
+ const tier = getItem(itemId).tier
246
+ out += ` Mass ${formatMassTonnes(computeLoaderMass(fin))} Thrust ${computeLoaderThrust(pla, tier)}`
145
247
  break
146
248
  }
147
249
  case MODULE_CRAFTER: {
148
250
  const rea = decodeStat(stats, 0)
149
- const com = decodeStat(stats, 1)
150
- out += ` Speed ${computeCrafterSpeed(rea)} Drain ${computeCrafterDrain(com)}`
251
+ const con = decodeStat(stats, 1)
252
+ const tier = getItem(itemId).tier
253
+ out += ` Speed ${computeCrafterSpeed(rea, tier)} Drain ${toWholeEnergy(computeCrafterDrain(con))}`
254
+ break
255
+ }
256
+ case MODULE_BUILDER: {
257
+ const res = decodeStat(stats, 0)
258
+ const fin = decodeStat(stats, 1)
259
+ const tier = getItem(itemId).tier
260
+ out += ` Speed ${computeBuilderSpeed(res, tier)} Drain ${toWholeEnergy(computeBuilderDrain(fin))}`
151
261
  break
152
262
  }
153
263
  case MODULE_STORAGE: {
154
264
  const str = decodeStat(stats, 0)
155
- const fin = decodeStat(stats, 2)
156
- const sat = decodeStat(stats, 3)
157
- const sum = str + fin + sat
158
- const pct = 10 + idiv(sum * 10, 2997)
159
- out += ` +${pct}% capacity`
265
+ const den = decodeStat(stats, 1)
266
+ const hrd = decodeStat(stats, 2)
267
+ const coh = decodeStat(stats, 3)
268
+ const tier = getItem(itemId).tier
269
+ out += ` Cargo Capacity ${formatMassTonnes(computeCargoBayCapacity(str, den, hrd, tier))} Drain ${toWholeEnergy(computeCargoBayDrain(coh, tier))}`
160
270
  break
161
271
  }
162
272
  case MODULE_HAULER: {
163
- const fin = decodeStat(stats, 0)
164
- const con = decodeStat(stats, 1)
165
- const com = decodeStat(stats, 2)
166
- out += ` Capacity ${computeHaulerCapacity(fin)} Efficiency ${computeHaulerEfficiency(con)} Drain ${computeHaulerDrain(com)}`
273
+ const res = decodeStat(stats, 0)
274
+ const pla = decodeStat(stats, 1)
275
+ const con = decodeStat(stats, 2)
276
+ const tier = getItem(itemId).tier
277
+ out += ` Capacity ${computeHaulerCapacity(res, tier)} Efficiency ${computeHaulerEfficiency(pla)} Drain ${toWholeEnergy(computeHaulerDrain(con, tier))}`
167
278
  break
168
279
  }
169
280
  case MODULE_WARP: {
170
281
  const stat = decodeStat(stats, 0)
171
- out += ` Range ${computeWarpRange(stat)}`
282
+ const tier = getItem(itemId).tier
283
+ out += ` Range ${computeWarpRange(stat, tier)}`
284
+ break
285
+ }
286
+ case MODULE_BATTERY: {
287
+ const vol = decodeStat(stats, 0)
288
+ const thm = decodeStat(stats, 1)
289
+ const pla = decodeStat(stats, 2)
290
+ const ins = decodeStat(stats, 3)
291
+ const tier = getItem(itemId).tier
292
+ out += ` Energy Capacity ${toWholeEnergy(computeBatteryBankCapacity(vol, thm, pla, ins, tier))}`
172
293
  break
173
294
  }
174
295
  }
@@ -181,18 +302,13 @@ export function buildEntityDescription(
181
302
  moduleItems: number[],
182
303
  moduleStats: bigint[]
183
304
  ): string {
184
- const hullMass = computeBaseHullmass(hullStats)
185
- let baseCapacity = 0
186
- if (itemId === ITEM_SHIP_T1_PACKED) {
187
- baseCapacity = computeBaseCapacityShip(hullStats)
188
- } else if (itemId === ITEM_WAREHOUSE_T1_PACKED) {
189
- baseCapacity = computeBaseCapacityWarehouse(hullStats)
190
- }
305
+ const hullMass = computeBaseHullmass(itemId, hullStats)
306
+ const baseCapacity = computeBaseCapacityForEntity(itemId, hullStats)
191
307
 
192
308
  let out = entityDisplayName(itemId)
193
- out += ` - Hull ${hullMass} mass`
309
+ out += ` - Hull ${formatMassTonnes(hullMass)}`
194
310
  if (baseCapacity > 0) {
195
- out += ` * ${baseCapacity} capacity`
311
+ out += ` * ${formatMassTonnes(baseCapacity)} capacity`
196
312
  }
197
313
  out += '\n\n'
198
314
 
package/src/nft/index.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  export * from './deserializers'
2
2
  export * from './description'
3
+ export * from './atomicdata'
4
+ export * from './atomicassets'
5
+ export * from './buildImmutableData'
@@ -0,0 +1,242 @@
1
+ import type {GathererStats} from '../types/capabilities'
2
+ import type {ServerContract} from '../contracts'
3
+ import {calc_gather_duration, calc_gather_energy} from '../capabilities/gathering'
4
+ import {calc_rechargetime} from '../travel/travel'
5
+ import {projectRemainingAt, type Projectable} from '../scheduling/projection'
6
+
7
+ export interface LanePlanEntry {
8
+ slot: number
9
+ quantity: number
10
+ }
11
+
12
+ export type PlanTarget = {quantity: number} | 'max'
13
+
14
+ export interface GatherPlanEntity extends Projectable {
15
+ gatherer_lanes: ServerContract.Types.gatherer_lane[]
16
+ loader_lanes: ServerContract.Types.loader_lane[]
17
+ }
18
+
19
+ export interface GatherLimpet {
20
+ slot: number
21
+ quantity: number
22
+ durationSeconds: number
23
+ }
24
+
25
+ export interface GatherCycle {
26
+ rechargeBefore: boolean
27
+ rechargeSeconds: number
28
+ limpets: GatherLimpet[]
29
+ gatherSeconds: number
30
+ batchOre: number
31
+ }
32
+
33
+ export type FillCap = 'reserve' | 'hold' | 'requested'
34
+
35
+ export interface GatherPlan {
36
+ cycles: GatherCycle[]
37
+ cycleCount: number
38
+ totalOre: number
39
+ totalSeconds: number
40
+ cap: FillCap
41
+ reachingCount: number
42
+ totalLimpets: number
43
+ warnings: string[]
44
+ }
45
+
46
+ export interface BuildGatherPlanOpts {
47
+ richness: number
48
+ itemMass: number
49
+ holdRoom: number
50
+ reserveRemaining: number
51
+ now: Date
52
+ }
53
+
54
+ const MAX_CYCLES = 10_000
55
+
56
+ const MAX_TRANSFER_QTY = 10000
57
+
58
+ export function allocateProportional(
59
+ lanes: {slot: number; weight: number}[],
60
+ total: number
61
+ ): LanePlanEntry[] {
62
+ if (lanes.length === 0) return []
63
+ const weightSum = lanes.reduce((s, l) => s + l.weight, 0)
64
+ if (weightSum === 0) return []
65
+
66
+ const entries: LanePlanEntry[] = lanes.map((l) => ({
67
+ slot: l.slot,
68
+ quantity: Math.floor((total * l.weight) / weightSum),
69
+ }))
70
+
71
+ let remainder = total - entries.reduce((s, e) => s + e.quantity, 0)
72
+ for (let i = 0; remainder > 0; i = (i + 1) % entries.length) {
73
+ entries[i].quantity++
74
+ remainder--
75
+ }
76
+
77
+ return entries
78
+ }
79
+
80
+ function laneStats(lane: ServerContract.Types.gatherer_lane): GathererStats {
81
+ return lane as unknown as GathererStats
82
+ }
83
+
84
+ export function gatherEnergyCost(
85
+ lane: ServerContract.Types.gatherer_lane,
86
+ quantity: number,
87
+ stratum: number,
88
+ itemMass: number,
89
+ richness: number
90
+ ): number {
91
+ if (quantity <= 0) return 0
92
+ const dur = Number(calc_gather_duration(laneStats(lane), itemMass, quantity, stratum, richness))
93
+ return Number(calc_gather_energy(laneStats(lane), dur))
94
+ }
95
+
96
+ export function splitCost(
97
+ reaching: ServerContract.Types.gatherer_lane[],
98
+ quantity: number,
99
+ stratum: number,
100
+ itemMass: number,
101
+ richness: number
102
+ ): number {
103
+ if (quantity <= 0) return 0
104
+ const weights = reaching.map((l) => ({
105
+ slot: l.slot_index.toNumber(),
106
+ weight: l.yield.toNumber(),
107
+ }))
108
+ const split = allocateProportional(weights, quantity)
109
+ return split.reduce((sum, e) => {
110
+ const lane = reaching.find((l) => l.slot_index.toNumber() === e.slot)!
111
+ return sum + gatherEnergyCost(lane, e.quantity, stratum, itemMass, richness)
112
+ }, 0)
113
+ }
114
+
115
+ // Binary search is exact because splitCost is monotonic non-decreasing in Q.
116
+ export function maxQtyForCharge(
117
+ reaching: ServerContract.Types.gatherer_lane[],
118
+ hi: number,
119
+ capacity: number,
120
+ stratum: number,
121
+ itemMass: number,
122
+ richness: number
123
+ ): number {
124
+ if (hi <= 0) return 0
125
+ if (splitCost(reaching, hi, stratum, itemMass, richness) <= capacity) return hi
126
+ let lo = 0
127
+ let high = hi
128
+ while (lo < high) {
129
+ const mid = Math.ceil((lo + high) / 2)
130
+ if (splitCost(reaching, mid, stratum, itemMass, richness) <= capacity) lo = mid
131
+ else high = mid - 1
132
+ }
133
+ return lo
134
+ }
135
+
136
+ export function buildGatherPlan(
137
+ entity: GatherPlanEntity,
138
+ stratum: number,
139
+ target: PlanTarget,
140
+ opts: BuildGatherPlanOpts
141
+ ): GatherPlan {
142
+ const {richness, itemMass, holdRoom, reserveRemaining, now} = opts
143
+ const warnings: string[] = []
144
+
145
+ const reaching = entity.gatherer_lanes.filter((l) => l.depth.toNumber() >= stratum)
146
+ if (reaching.length === 0) throw new Error('no gatherer reaches this stratum')
147
+ if (!entity.generator) throw new Error('entity has no generator')
148
+
149
+ const blocked = entity.gatherer_lanes.length - reaching.length
150
+ if (blocked > 0) {
151
+ warnings.push(
152
+ `${blocked} of ${entity.gatherer_lanes.length} limpets can't reach this depth`
153
+ )
154
+ }
155
+
156
+ const capacity = entity.generator.capacity.toNumber()
157
+ const rechargeRate = entity.generator.recharge.toNumber()
158
+ let energy = Number(projectRemainingAt(entity, now).energy)
159
+
160
+ const requested = target === 'max' ? Infinity : target.quantity
161
+ let fillTarget = requested
162
+ let cap: FillCap = 'requested'
163
+ if (holdRoom < fillTarget) {
164
+ fillTarget = holdRoom
165
+ cap = 'hold'
166
+ }
167
+ if (reserveRemaining < fillTarget) {
168
+ fillTarget = reserveRemaining
169
+ cap = 'reserve'
170
+ }
171
+ fillTarget = Math.max(0, Math.floor(fillTarget))
172
+
173
+ const cycles: GatherCycle[] = []
174
+ let remaining = fillTarget
175
+ let guard = 0
176
+ while (remaining > 0 && guard++ < MAX_CYCLES) {
177
+ const batch = maxQtyForCharge(reaching, remaining, capacity, stratum, itemMass, richness)
178
+ if (batch <= 0) {
179
+ warnings.push('a single gather cannot fit within one full charge')
180
+ break
181
+ }
182
+
183
+ const costFull = splitCost(reaching, batch, stratum, itemMass, richness)
184
+ const rechargeBefore = energy < costFull
185
+ const rechargeSeconds = rechargeBefore
186
+ ? Number(calc_rechargetime(capacity, energy, rechargeRate))
187
+ : 0
188
+ if (rechargeBefore) energy = capacity
189
+
190
+ const weights = reaching.map((l) => ({
191
+ slot: l.slot_index.toNumber(),
192
+ weight: l.yield.toNumber(),
193
+ }))
194
+ const split = allocateProportional(weights, batch).filter((e) => e.quantity > 0)
195
+ const limpets: GatherLimpet[] = split.map((e) => {
196
+ const lane = reaching.find((l) => l.slot_index.toNumber() === e.slot)!
197
+ const dur = Number(
198
+ calc_gather_duration(laneStats(lane), itemMass, e.quantity, stratum, richness)
199
+ )
200
+ return {slot: e.slot, quantity: e.quantity, durationSeconds: dur}
201
+ })
202
+ const actualCost = limpets.reduce((sum, l) => {
203
+ const lane = reaching.find((r) => r.slot_index.toNumber() === l.slot)!
204
+ return sum + Number(calc_gather_energy(laneStats(lane), l.durationSeconds))
205
+ }, 0)
206
+ energy = Math.max(0, energy - actualCost)
207
+
208
+ const gatherSeconds = limpets.reduce((m, l) => Math.max(m, l.durationSeconds), 0)
209
+ cycles.push({rechargeBefore, rechargeSeconds, limpets, gatherSeconds, batchOre: batch})
210
+ remaining -= batch
211
+ }
212
+
213
+ const totalOre = cycles.reduce((s, c) => s + c.batchOre, 0)
214
+ const totalSeconds = cycles.reduce((s, c) => s + c.rechargeSeconds + c.gatherSeconds, 0)
215
+ return {
216
+ cycles,
217
+ cycleCount: cycles.length,
218
+ totalOre,
219
+ totalSeconds,
220
+ cap,
221
+ reachingCount: reaching.length,
222
+ totalLimpets: entity.gatherer_lanes.length,
223
+ warnings,
224
+ }
225
+ }
226
+
227
+ export function planParallelTransfer(
228
+ entity: GatherPlanEntity,
229
+ target: PlanTarget
230
+ ): LanePlanEntry[] {
231
+ const lanes = entity.loader_lanes.filter((l) => l.thrust.toNumber() > 0)
232
+ if (lanes.length === 0) return []
233
+
234
+ const requestedQty = target === 'max' ? MAX_TRANSFER_QTY : target.quantity
235
+
236
+ const laneWeights = lanes.map((l) => ({
237
+ slot: l.slot_index.toNumber(),
238
+ weight: l.thrust.toNumber(),
239
+ }))
240
+
241
+ return allocateProportional(laneWeights, requestedQty).filter((e) => e.quantity > 0)
242
+ }