@shipload/sdk 1.0.0-next.1 → 1.0.0-next.11

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 (56) hide show
  1. package/lib/shipload.d.ts +618 -353
  2. package/lib/shipload.js +2250 -1059
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +2193 -1044
  5. package/lib/shipload.m.js.map +1 -1
  6. package/package.json +2 -2
  7. package/src/capabilities/modules.ts +3 -0
  8. package/src/capabilities/storage.ts +1 -1
  9. package/src/contracts/platform.ts +13 -1
  10. package/src/contracts/server.ts +227 -282
  11. package/src/data/capabilities.ts +5 -330
  12. package/src/data/capability-formulas.ts +70 -0
  13. package/src/data/catalog.ts +0 -5
  14. package/src/data/colors.ts +2 -16
  15. package/src/data/entities.json +46 -10
  16. package/src/data/item-ids.ts +4 -1
  17. package/src/data/items.json +264 -0
  18. package/src/data/metadata.ts +63 -1
  19. package/src/data/recipes-runtime.ts +1 -0
  20. package/src/data/recipes.json +139 -11
  21. package/src/derivation/capability-mappings.ts +122 -0
  22. package/src/derivation/index.ts +1 -0
  23. package/src/derivation/resources.ts +116 -37
  24. package/src/derivation/stats.ts +1 -2
  25. package/src/entities/container.ts +25 -10
  26. package/src/entities/extractor.ts +144 -0
  27. package/src/entities/factory.ts +135 -0
  28. package/src/entities/gamestate.ts +0 -9
  29. package/src/entities/makers.ts +130 -20
  30. package/src/entities/nexus.ts +29 -0
  31. package/src/entities/ship-deploy.ts +114 -56
  32. package/src/entities/ship.ts +17 -0
  33. package/src/entities/slot-multiplier.ts +21 -0
  34. package/src/entities/warehouse.ts +20 -3
  35. package/src/errors.ts +10 -13
  36. package/src/index-module.ts +81 -26
  37. package/src/managers/actions.ts +53 -80
  38. package/src/managers/entities.ts +65 -17
  39. package/src/managers/locations.ts +2 -20
  40. package/src/nft/atomicdata.ts +128 -0
  41. package/src/nft/description.ts +41 -7
  42. package/src/nft/index.ts +1 -0
  43. package/src/resolution/resolve-item.ts +17 -9
  44. package/src/scheduling/accessor.ts +4 -0
  45. package/src/scheduling/projection.ts +10 -2
  46. package/src/scheduling/schedule.ts +15 -1
  47. package/src/scheduling/task-cargo.ts +47 -0
  48. package/src/subscriptions/connection.ts +50 -2
  49. package/src/subscriptions/manager.ts +84 -4
  50. package/src/subscriptions/mappers.ts +5 -1
  51. package/src/subscriptions/types.ts +2 -2
  52. package/src/travel/travel.ts +61 -2
  53. package/src/types/entity-traits.ts +103 -1
  54. package/src/types.ts +11 -1
  55. package/src/utils/cargo.ts +27 -0
  56. package/src/utils/system.ts +25 -24
@@ -58,13 +58,6 @@ export class GameState extends ServerContract.Types.state_row {
58
58
  return this.enabled
59
59
  }
60
60
 
61
- /**
62
- * Get the total number of ships in the game
63
- */
64
- get shipCount(): number {
65
- return Number(this.ships)
66
- }
67
-
68
61
  /**
69
62
  * Get the current salt value (used for random number generation)
70
63
  */
@@ -137,14 +130,12 @@ export class GameState extends ServerContract.Types.state_row {
137
130
  get summary(): {
138
131
  enabled: boolean
139
132
  epoch: string
140
- ships: number
141
133
  hasSeed: boolean
142
134
  hasCommit: boolean
143
135
  } {
144
136
  return {
145
137
  enabled: this.enabled,
146
138
  epoch: this.epoch.toString(),
147
- ships: this.shipCount,
148
139
  hasSeed: !this.seed.equals(Checksum256.from('0'.repeat(64))),
149
140
  hasCommit: !this.commit.equals(Checksum256.from('0'.repeat(64))),
150
141
  }
@@ -3,8 +3,16 @@ import {ServerContract} from '../contracts'
3
3
  import {type PackedModuleInput, Ship, type ShipStateInput} from './ship'
4
4
  import {computeWarehouseCapabilities, Warehouse, type WarehouseStateInput} from './warehouse'
5
5
  import {Container, type ContainerStateInput} from './container'
6
- import {ITEM_SHIP_T1_PACKED, ITEM_WAREHOUSE_T1_PACKED} from '../data/item-ids'
7
- import {getEntityLayout} from '../data/recipes-runtime'
6
+ import {Nexus, type NexusStateInput} from './nexus'
7
+ import {Extractor, computeExtractorCapabilities, type ExtractorStateInput} from './extractor'
8
+ import {Factory, computeFactoryCapabilities, type FactoryStateInput} from './factory'
9
+ import {
10
+ ITEM_EXTRACTOR_T1_PACKED,
11
+ ITEM_FACTORY_T1_PACKED,
12
+ ITEM_SHIP_T1_PACKED,
13
+ ITEM_WAREHOUSE_T1_PACKED,
14
+ } from '../data/item-ids'
15
+ import {getEntityLayout, type EntitySlot} from '../data/recipes-runtime'
8
16
  import {itemMetadata} from '../data/metadata'
9
17
  import {getItem} from '../data/catalog'
10
18
  import {
@@ -14,6 +22,7 @@ import {
14
22
  moduleSlotTypeToCode,
15
23
  } from '../capabilities/modules'
16
24
  import {computeShipCapabilities, computeStorageCapabilities} from './ship-deploy'
25
+ import type {InstalledModule} from './slot-multiplier'
17
26
  import {decodeCraftedItemStats} from '../derivation/crafting'
18
27
 
19
28
  function assignModulesToSlots(
@@ -56,19 +65,22 @@ function assignModulesToSlots(
56
65
  )
57
66
  }
58
67
 
59
- function decodePackedInput(m: PackedModuleInput): {itemId: number; stats: bigint} {
60
- return {
61
- itemId: Number(UInt16.from(m.itemId).value.toString()),
62
- stats: BigInt(UInt64.from(m.stats).toString()),
63
- }
68
+ function toInstalledModules(entries: ServerContract.Types.module_entry[]): InstalledModule[] {
69
+ const installed: InstalledModule[] = []
70
+ entries.forEach((entry, slotIndex) => {
71
+ if (!entry.installed) return
72
+ installed.push({
73
+ slotIndex,
74
+ itemId: Number(UInt16.from(entry.installed.item_id).value.toString()),
75
+ stats: BigInt(UInt64.from(entry.installed.stats).toString()),
76
+ })
77
+ })
78
+ return installed
64
79
  }
65
80
 
66
- function computeStorageBonus(
67
- decoded: {itemId: number; stats: bigint}[],
68
- baseCapacity: number
69
- ): number {
81
+ function computeStorageBonus(modules: InstalledModule[], baseCapacity: number): number {
70
82
  let totalBonus = 0
71
- for (const m of decoded) {
83
+ for (const m of modules) {
72
84
  if (getModuleCapabilityType(m.itemId) !== MODULE_STORAGE) continue
73
85
  const stats = decodeCraftedItemStats(m.itemId, m.stats)
74
86
  const {capacityBonus} = computeStorageCapabilities(stats, baseCapacity)
@@ -78,15 +90,16 @@ function computeStorageBonus(
78
90
  }
79
91
 
80
92
  function deriveShipFromModules(
81
- modules: PackedModuleInput[],
93
+ moduleEntries: ServerContract.Types.module_entry[],
94
+ layout: EntitySlot[],
82
95
  baseCapacity: number
83
96
  ): {
84
97
  capabilities: ReturnType<typeof computeShipCapabilities>
85
98
  finalCapacity: number
86
99
  } {
87
- const decoded = modules.map(decodePackedInput)
88
- const capabilities = computeShipCapabilities(decoded)
89
- const totalBonus = computeStorageBonus(decoded, baseCapacity)
100
+ const installed = toInstalledModules(moduleEntries)
101
+ const capabilities = computeShipCapabilities(installed, layout)
102
+ const totalBonus = computeStorageBonus(installed, baseCapacity)
90
103
  return {capabilities, finalCapacity: baseCapacity + totalBonus}
91
104
  }
92
105
 
@@ -109,10 +122,12 @@ export function makeShip(state: ShipStateInput): Ship {
109
122
  if (state.schedule) info.schedule = state.schedule
110
123
 
111
124
  let moduleEntries: ServerContract.Types.module_entry[] = []
125
+ const shipLayout = getEntityLayout(ITEM_SHIP_T1_PACKED)?.slots ?? []
112
126
  if (state.modules && state.modules.length > 0) {
113
127
  moduleEntries = assignModulesToSlots(ITEM_SHIP_T1_PACKED, state.modules, 'Ship T1')
114
128
  const {capabilities, finalCapacity} = deriveShipFromModules(
115
- state.modules,
129
+ moduleEntries,
130
+ shipLayout,
116
131
  state.capacity ?? 0
117
132
  )
118
133
  if (capabilities.engines) info.engines = capabilities.engines
@@ -152,17 +167,18 @@ export function makeWarehouse(state: WarehouseStateInput): Warehouse {
152
167
  if (state.schedule) info.schedule = state.schedule
153
168
 
154
169
  let moduleEntries: ServerContract.Types.module_entry[] = []
170
+ const warehouseLayout = getEntityLayout(ITEM_WAREHOUSE_T1_PACKED)?.slots ?? []
155
171
  if (state.modules && state.modules.length > 0) {
156
172
  moduleEntries = assignModulesToSlots(
157
173
  ITEM_WAREHOUSE_T1_PACKED,
158
174
  state.modules,
159
175
  'Warehouse T1'
160
176
  )
161
- const decoded = state.modules.map(decodePackedInput)
162
- const capabilities = computeWarehouseCapabilities(decoded)
177
+ const installed = toInstalledModules(moduleEntries)
178
+ const capabilities = computeWarehouseCapabilities(installed, warehouseLayout)
163
179
  if (capabilities.loaders) info.loaders = capabilities.loaders
164
180
 
165
- const totalBonus = computeStorageBonus(decoded, state.capacity)
181
+ const totalBonus = computeStorageBonus(installed, state.capacity)
166
182
  info.capacity = UInt32.from(state.capacity + totalBonus)
167
183
  } else {
168
184
  moduleEntries = assignModulesToSlots(ITEM_WAREHOUSE_T1_PACKED, [], 'Warehouse T1')
@@ -174,6 +190,82 @@ export function makeWarehouse(state: WarehouseStateInput): Warehouse {
174
190
  return new Warehouse(entityInfo)
175
191
  }
176
192
 
193
+ export function makeExtractor(state: ExtractorStateInput): Extractor {
194
+ const info: Record<string, unknown> = {
195
+ type: Name.from('extractor'),
196
+ id: UInt64.from(state.id),
197
+ owner: Name.from(state.owner),
198
+ entity_name: state.name,
199
+ coordinates: ServerContract.Types.coordinates.from(state.coordinates),
200
+ cargomass: UInt32.from(0),
201
+ cargo: state.cargo || [],
202
+ is_idle: !state.schedule,
203
+ current_task_elapsed: UInt32.from(0),
204
+ current_task_remaining: UInt32.from(0),
205
+ pending_tasks: [],
206
+ }
207
+ if (state.hullmass !== undefined) info.hullmass = UInt32.from(state.hullmass)
208
+ if (state.energy !== undefined) info.energy = UInt16.from(state.energy)
209
+ if (state.schedule) info.schedule = state.schedule
210
+ if (state.capacity !== undefined) info.capacity = UInt32.from(state.capacity)
211
+
212
+ const moduleEntries = assignModulesToSlots(
213
+ ITEM_EXTRACTOR_T1_PACKED,
214
+ state.modules ?? [],
215
+ 'Extractor T1'
216
+ )
217
+ if (state.modules && state.modules.length > 0) {
218
+ const layout = getEntityLayout(ITEM_EXTRACTOR_T1_PACKED)?.slots ?? []
219
+ const installed = toInstalledModules(moduleEntries)
220
+ const capabilities = computeExtractorCapabilities(installed, layout)
221
+ if (capabilities.generator) info.generator = capabilities.generator
222
+ if (capabilities.gatherer) info.gatherer = capabilities.gatherer
223
+ }
224
+
225
+ info.modules = moduleEntries
226
+
227
+ const entityInfo = ServerContract.Types.entity_info.from(info)
228
+ return new Extractor(entityInfo)
229
+ }
230
+
231
+ export function makeFactory(state: FactoryStateInput): Factory {
232
+ const info: Record<string, unknown> = {
233
+ type: Name.from('factory'),
234
+ id: UInt64.from(state.id),
235
+ owner: Name.from(state.owner),
236
+ entity_name: state.name,
237
+ coordinates: ServerContract.Types.coordinates.from(state.coordinates),
238
+ cargomass: UInt32.from(0),
239
+ cargo: state.cargo || [],
240
+ is_idle: !state.schedule,
241
+ current_task_elapsed: UInt32.from(0),
242
+ current_task_remaining: UInt32.from(0),
243
+ pending_tasks: [],
244
+ }
245
+ if (state.hullmass !== undefined) info.hullmass = UInt32.from(state.hullmass)
246
+ if (state.energy !== undefined) info.energy = UInt16.from(state.energy)
247
+ if (state.schedule) info.schedule = state.schedule
248
+ if (state.capacity !== undefined) info.capacity = UInt32.from(state.capacity)
249
+
250
+ const moduleEntries = assignModulesToSlots(
251
+ ITEM_FACTORY_T1_PACKED,
252
+ state.modules ?? [],
253
+ 'Factory T1'
254
+ )
255
+ if (state.modules && state.modules.length > 0) {
256
+ const layout = getEntityLayout(ITEM_FACTORY_T1_PACKED)?.slots ?? []
257
+ const installed = toInstalledModules(moduleEntries)
258
+ const capabilities = computeFactoryCapabilities(installed, layout)
259
+ if (capabilities.generator) info.generator = capabilities.generator
260
+ if (capabilities.crafter) info.crafter = capabilities.crafter
261
+ }
262
+
263
+ info.modules = moduleEntries
264
+
265
+ const entityInfo = ServerContract.Types.entity_info.from(info)
266
+ return new Factory(entityInfo)
267
+ }
268
+
177
269
  export function makeContainer(state: ContainerStateInput): Container {
178
270
  const entityInfo = ServerContract.Types.entity_info.from({
179
271
  type: Name.from('container'),
@@ -194,3 +286,21 @@ export function makeContainer(state: ContainerStateInput): Container {
194
286
  })
195
287
  return new Container(entityInfo)
196
288
  }
289
+
290
+ export function makeNexus(state: NexusStateInput): Nexus {
291
+ const entityInfo = ServerContract.Types.entity_info.from({
292
+ type: Name.from('nexus'),
293
+ id: UInt64.from(state.id),
294
+ owner: Name.from(state.owner),
295
+ entity_name: state.name,
296
+ coordinates: ServerContract.Types.coordinates.from(state.coordinates),
297
+ cargomass: UInt32.from(0),
298
+ cargo: [],
299
+ modules: [],
300
+ is_idle: true,
301
+ current_task_elapsed: UInt32.from(0),
302
+ current_task_remaining: UInt32.from(0),
303
+ pending_tasks: [],
304
+ })
305
+ return new Nexus(entityInfo)
306
+ }
@@ -0,0 +1,29 @@
1
+ import type {UInt64Type} from '@wharfkit/antelope'
2
+ import {ServerContract} from '../contracts'
3
+ import type {CoordinatesType} from '../types'
4
+ import {Location} from './location'
5
+
6
+ export interface NexusStateInput {
7
+ id: UInt64Type
8
+ owner: string
9
+ name: string
10
+ coordinates: CoordinatesType | {x: number; y: number; z?: number}
11
+ }
12
+
13
+ export class Nexus extends ServerContract.Types.entity_info {
14
+ get name(): string {
15
+ return this.entity_name
16
+ }
17
+
18
+ get entityClass(): 'orbital' {
19
+ return 'orbital'
20
+ }
21
+
22
+ get location(): Location {
23
+ return Location.from(this.coordinates)
24
+ }
25
+
26
+ get orbitalAltitude(): number {
27
+ return this.coordinates.z?.toNumber() || 0
28
+ }
29
+ }
@@ -8,17 +8,22 @@ import {
8
8
  MODULE_HAULER,
9
9
  MODULE_LOADER,
10
10
  } from '../capabilities/modules'
11
+ import {getItem} from '../data/catalog'
12
+ import type {EntitySlot} from '../data/recipes-runtime'
13
+ import {applySlotMultiplier, clampUint16, getSlotAmp, type InstalledModule} from './slot-multiplier'
14
+
15
+ export type {InstalledModule}
11
16
 
12
17
  export function computeShipHullCapabilities(stats: Record<string, number>): {
13
18
  hullmass: number
14
19
  capacity: number
15
20
  } {
16
- const density = stats.density ?? 500
17
- const strength = stats.strength ?? 500
18
- const hardness = stats.hardness ?? 500
19
- const saturation = stats.saturation ?? 500
21
+ const density = stats.density
22
+ const strength = stats.strength
23
+ const hardness = stats.hardness
24
+ const saturation = stats.saturation
20
25
 
21
- const hullmass = 25000 + 75 * density
26
+ const hullmass = 100000 - 75 * density
22
27
  const statSum = strength + hardness + saturation
23
28
  const exponent = statSum / 2997.0
24
29
  const capacity = Math.floor(1000000 * 10 ** exponent)
@@ -30,8 +35,8 @@ export function computeEngineCapabilities(stats: Record<string, number>): {
30
35
  thrust: number
31
36
  drain: number
32
37
  } {
33
- const vol = stats.volatility ?? 500
34
- const thm = stats.thermal ?? 500
38
+ const vol = stats.volatility
39
+ const thm = stats.thermal
35
40
 
36
41
  return {
37
42
  thrust: 400 + Math.floor((vol * 3) / 4),
@@ -43,30 +48,61 @@ export function computeGeneratorCapabilities(stats: Record<string, number>): {
43
48
  capacity: number
44
49
  recharge: number
45
50
  } {
46
- const res = stats.resonance ?? 500
47
- const ref = stats.reflectivity ?? 500
51
+ const com = stats.composition
52
+ const fin = stats.fineness
48
53
 
49
54
  return {
50
- capacity: 300 + Math.floor(res / 6),
51
- recharge: 1 + Math.floor((ref * 3) / 1000),
55
+ capacity: 300 + Math.floor(com / 6),
56
+ recharge: 1 + Math.floor((fin * 3) / 1000),
57
+ }
58
+ }
59
+
60
+ export interface GathererDepthParams {
61
+ readonly floor: number
62
+ readonly slope: number
63
+ }
64
+
65
+ export const GATHERER_DEPTH_TABLE: readonly GathererDepthParams[] = [
66
+ {floor: 500, slope: 5},
67
+ {floor: 2000, slope: 11},
68
+ {floor: 7000, slope: 16},
69
+ {floor: 15000, slope: 18},
70
+ {floor: 25000, slope: 19},
71
+ {floor: 35000, slope: 16},
72
+ {floor: 46000, slope: 12},
73
+ {floor: 53500, slope: 10},
74
+ {floor: 60000, slope: 5},
75
+ {floor: 63500, slope: 2},
76
+ ]
77
+
78
+ export const GATHERER_DEPTH_MAX_TIER = 10
79
+
80
+ export function gathererDepthForTier(tol: number, tier: number): number {
81
+ if (tier < 1 || tier > GATHERER_DEPTH_MAX_TIER) {
82
+ throw new Error(`gatherer tier out of range: ${tier}`)
52
83
  }
84
+ const p = GATHERER_DEPTH_TABLE[tier - 1]
85
+ return p.floor + tol * p.slope
53
86
  }
54
87
 
55
- export function computeGathererCapabilities(stats: Record<string, number>): {
88
+ export function computeGathererCapabilities(
89
+ stats: Record<string, number>,
90
+ tier: number
91
+ ): {
56
92
  yield: number
57
93
  drain: number
58
94
  depth: number
59
95
  speed: number
60
96
  } {
61
- const str = stats.strength ?? 500
62
- const con = stats.conductivity ?? 500
63
- const ref = stats.reflectivity ?? 500
64
- const tol = stats.tolerance ?? 500
97
+ const str = stats.strength
98
+ const con = stats.conductivity
99
+ const ref = stats.reflectivity
100
+ const tol = stats.tolerance
65
101
 
66
102
  return {
67
103
  yield: 200 + str,
68
104
  drain: Math.max(250, 1250 - Math.floor((con * 25) / 20)),
69
- depth: 200 + Math.floor((tol * 3) / 2),
105
+ depth: gathererDepthForTier(tol, tier),
70
106
  speed: 100 + Math.floor((ref * 4) / 5),
71
107
  }
72
108
  }
@@ -76,12 +112,12 @@ export function computeLoaderCapabilities(stats: Record<string, number>): {
76
112
  thrust: number
77
113
  quantity: number
78
114
  } {
79
- const hrd = stats.hardness ?? 500
80
- const pla = stats.plasticity ?? 500
115
+ const insulation = stats.insulation
116
+ const plasticity = stats.plasticity
81
117
 
82
118
  return {
83
- mass: Math.max(200, 2000 - Math.floor(hrd * 2)),
84
- thrust: 1 + Math.floor(pla / 500),
119
+ mass: Math.max(200, 2000 - Math.floor(insulation * 2)),
120
+ thrust: 1 + Math.floor(plasticity / 500),
85
121
  quantity: 1,
86
122
  }
87
123
  }
@@ -90,12 +126,12 @@ export function computeCrafterCapabilities(stats: Record<string, number>): {
90
126
  speed: number
91
127
  drain: number
92
128
  } {
93
- const rea = stats.reactivity ?? 500
94
- const com = stats.composition ?? 500
129
+ const rea = stats.reactivity
130
+ const fin = stats.fineness
95
131
 
96
132
  return {
97
133
  speed: 100 + Math.floor((rea * 4) / 5),
98
- drain: Math.max(5, 30 - Math.floor(com / 33)),
134
+ drain: Math.max(5, 30 - Math.floor(fin / 33)),
99
135
  }
100
136
  }
101
137
 
@@ -104,14 +140,14 @@ export function computeHaulerCapabilities(stats: Record<string, number>): {
104
140
  efficiency: number
105
141
  drain: number
106
142
  } {
107
- const res = stats.resonance ?? 500
108
- const con = stats.conductivity ?? 500
109
- const ref = stats.reflectivity ?? 500
143
+ const fineness = stats.fineness
144
+ const conductivity = stats.conductivity
145
+ const composition = stats.composition
110
146
 
111
147
  return {
112
- capacity: Math.max(1, 1 + Math.floor(res / 400)),
113
- efficiency: 2000 + con * 6,
114
- drain: Math.max(3, 15 - Math.floor(ref / 80)),
148
+ capacity: Math.max(1, 1 + Math.floor(fineness / 400)),
149
+ efficiency: 2000 + conductivity * 6,
150
+ drain: Math.max(3, 15 - Math.floor(composition / 80)),
115
151
  }
116
152
  }
117
153
 
@@ -121,11 +157,12 @@ export function computeStorageCapabilities(
121
157
  ): {
122
158
  capacityBonus: number
123
159
  } {
124
- const strength = stats.strength ?? 500
125
- const hardness = stats.hardness ?? 500
126
- const saturation = stats.saturation ?? 500
160
+ const strength = stats.strength
161
+ const density = stats.density
162
+ const hardness = stats.hardness
163
+ const saturation = stats.saturation
127
164
 
128
- const statSum = strength + hardness + saturation
165
+ const statSum = strength + density + hardness + saturation
129
166
  const capacityBonus = Math.floor(
130
167
  (baseCapacity * (10 + Math.floor((statSum * 10) / 2997))) / 100
131
168
  )
@@ -137,12 +174,12 @@ export function computeWarehouseHullCapabilities(stats: Record<string, number>):
137
174
  hullmass: number
138
175
  capacity: number
139
176
  } {
140
- const density = stats.density ?? 500
141
- const strength = stats.strength ?? 500
142
- const hardness = stats.hardness ?? 500
143
- const saturation = stats.saturation ?? 500
177
+ const density = stats.density
178
+ const strength = stats.strength
179
+ const hardness = stats.hardness
180
+ const saturation = stats.saturation
144
181
 
145
- const hullmass = 25000 + 75 * density
182
+ const hullmass = 100000 - 75 * density
146
183
  const statSum = strength + hardness + saturation
147
184
  const exponent = statSum / 2997.0
148
185
  const capacity = Math.floor(20000000 * 10 ** exponent)
@@ -160,7 +197,8 @@ export interface ShipCapabilities {
160
197
  }
161
198
 
162
199
  export function computeShipCapabilities(
163
- modules: {itemId: number; stats: bigint}[]
200
+ modules: InstalledModule[],
201
+ layout: EntitySlot[]
164
202
  ): ShipCapabilities {
165
203
  const ship: ShipCapabilities = {}
166
204
 
@@ -170,7 +208,7 @@ export function computeShipCapabilities(
170
208
  let totalDrain = 0
171
209
  for (const m of engineModules) {
172
210
  const caps = computeEngineCapabilities(decodeCraftedItemStats(m.itemId, m.stats))
173
- totalThrust += caps.thrust
211
+ totalThrust += applySlotMultiplier(caps.thrust, getSlotAmp(layout, m.slotIndex))
174
212
  totalDrain += caps.drain
175
213
  }
176
214
  ship.engines = {thrust: totalThrust, drain: totalDrain}
@@ -184,10 +222,14 @@ export function computeShipCapabilities(
184
222
  let totalRecharge = 0
185
223
  for (const m of generatorModules) {
186
224
  const caps = computeGeneratorCapabilities(decodeCraftedItemStats(m.itemId, m.stats))
187
- totalCapacity += caps.capacity
188
- totalRecharge += caps.recharge
225
+ const amp = getSlotAmp(layout, m.slotIndex)
226
+ totalCapacity += applySlotMultiplier(caps.capacity, amp)
227
+ totalRecharge += applySlotMultiplier(caps.recharge, amp)
228
+ }
229
+ ship.generator = {
230
+ capacity: clampUint16(totalCapacity),
231
+ recharge: clampUint16(totalRecharge),
189
232
  }
190
- ship.generator = {capacity: totalCapacity, recharge: totalRecharge}
191
233
  }
192
234
 
193
235
  const gathererModules = modules.filter(
@@ -196,16 +238,26 @@ export function computeShipCapabilities(
196
238
  if (gathererModules.length > 0) {
197
239
  let totalYield = 0
198
240
  let totalDrain = 0
199
- let totalDepth = 0
241
+ let maxDepth = 0
200
242
  let totalSpeed = 0
201
243
  for (const m of gathererModules) {
202
- const caps = computeGathererCapabilities(decodeCraftedItemStats(m.itemId, m.stats))
203
- totalYield += caps.yield
244
+ const tier = getItem(m.itemId).tier
245
+ const caps = computeGathererCapabilities(
246
+ decodeCraftedItemStats(m.itemId, m.stats),
247
+ tier
248
+ )
249
+ const amp = getSlotAmp(layout, m.slotIndex)
250
+ totalYield += applySlotMultiplier(caps.yield, amp)
204
251
  totalDrain += caps.drain
205
- totalDepth += caps.depth
206
- totalSpeed += caps.speed
252
+ if (caps.depth > maxDepth) maxDepth = caps.depth
253
+ totalSpeed += applySlotMultiplier(caps.speed, amp)
254
+ }
255
+ ship.gatherer = {
256
+ yield: clampUint16(totalYield),
257
+ drain: totalDrain,
258
+ depth: maxDepth,
259
+ speed: clampUint16(totalSpeed),
207
260
  }
208
- ship.gatherer = {yield: totalYield, drain: totalDrain, depth: totalDepth, speed: totalSpeed}
209
261
  }
210
262
 
211
263
  const haulerModules = modules.filter((m) => getModuleCapabilityType(m.itemId) === MODULE_HAULER)
@@ -215,13 +267,15 @@ export function computeShipCapabilities(
215
267
  let totalDrain = 0
216
268
  for (const m of haulerModules) {
217
269
  const caps = computeHaulerCapabilities(decodeCraftedItemStats(m.itemId, m.stats))
270
+ const eff = applySlotMultiplier(caps.efficiency, getSlotAmp(layout, m.slotIndex))
218
271
  totalCapacity += caps.capacity
219
- weightedEffNum += caps.efficiency * caps.capacity
272
+ weightedEffNum += eff * caps.capacity
220
273
  totalDrain += caps.drain
221
274
  }
275
+ const efficiency = totalCapacity > 0 ? Math.floor(weightedEffNum / totalCapacity) : 0
222
276
  ship.hauler = {
223
277
  capacity: totalCapacity,
224
- efficiency: totalCapacity > 0 ? Math.floor(weightedEffNum / totalCapacity) : 0,
278
+ efficiency: clampUint16(efficiency),
225
279
  drain: totalDrain,
226
280
  }
227
281
  }
@@ -234,10 +288,14 @@ export function computeShipCapabilities(
234
288
  for (const m of loaderModules) {
235
289
  const caps = computeLoaderCapabilities(decodeCraftedItemStats(m.itemId, m.stats))
236
290
  totalMass += caps.mass
237
- totalThrust += caps.thrust
291
+ totalThrust += applySlotMultiplier(caps.thrust, getSlotAmp(layout, m.slotIndex))
238
292
  totalQuantity += caps.quantity
239
293
  }
240
- ship.loaders = {mass: totalMass, thrust: totalThrust, quantity: totalQuantity}
294
+ ship.loaders = {
295
+ mass: totalMass,
296
+ thrust: clampUint16(totalThrust),
297
+ quantity: totalQuantity,
298
+ }
241
299
  }
242
300
 
243
301
  const crafterModules = modules.filter(
@@ -248,10 +306,10 @@ export function computeShipCapabilities(
248
306
  let totalDrain = 0
249
307
  for (const m of crafterModules) {
250
308
  const caps = computeCrafterCapabilities(decodeCraftedItemStats(m.itemId, m.stats))
251
- totalSpeed += caps.speed
309
+ totalSpeed += applySlotMultiplier(caps.speed, getSlotAmp(layout, m.slotIndex))
252
310
  totalDrain += caps.drain
253
311
  }
254
- ship.crafter = {speed: totalSpeed, drain: totalDrain}
312
+ ship.crafter = {speed: clampUint16(totalSpeed), drain: totalDrain}
255
313
  }
256
314
 
257
315
  return ship
@@ -2,7 +2,9 @@ import {type UInt16, type UInt16Type, UInt32, UInt64, type UInt64Type} from '@wh
2
2
  import {ServerContract} from '../contracts'
3
3
  import {Coordinates, type CoordinatesType} from '../types'
4
4
  import {
5
+ type FloatPosition,
5
6
  getDestinationLocation,
7
+ getInterpolatedPosition,
6
8
  getPositionAt,
7
9
  getFlightOrigin as travelGetFlightOrigin,
8
10
  } from '../travel/travel'
@@ -55,6 +57,14 @@ export class Ship extends ServerContract.Types.entity_info {
55
57
  return this.entity_name
56
58
  }
57
59
 
60
+ get entityClass(): 'orbital' {
61
+ return 'orbital'
62
+ }
63
+
64
+ get canUndeploy(): boolean {
65
+ return true
66
+ }
67
+
58
68
  get inv(): InventoryAccessor {
59
69
  this._inv ??= new InventoryAccessor(this)
60
70
  return this._inv
@@ -87,12 +97,19 @@ export class Ship extends ServerContract.Types.entity_info {
87
97
  return dest ? Coordinates.from(dest) : undefined
88
98
  }
89
99
 
100
+ /** Chain-tile coordinates at `now`. For smooth visual position use interpolatedPositionAt. */
90
101
  positionAt(now: Date): Coordinates {
91
102
  const taskIndex = this.sched.currentTaskIndex(now)
92
103
  const progress = this.sched.currentTaskProgress(now)
93
104
  return Coordinates.from(getPositionAt(this, taskIndex, progress))
94
105
  }
95
106
 
107
+ interpolatedPositionAt(now: Date): FloatPosition {
108
+ const taskIndex = this.sched.currentTaskIndex(now)
109
+ const progress = this.sched.currentTaskProgressFloat(now)
110
+ return getInterpolatedPosition(this, taskIndex, progress)
111
+ }
112
+
96
113
  isInFlight(now: Date): boolean {
97
114
  return schedule.isInFlight(this, now)
98
115
  }
@@ -0,0 +1,21 @@
1
+ import type {EntitySlot} from '../data/recipes-runtime'
2
+
3
+ export const U16_MAX = 65535
4
+
5
+ export interface InstalledModule {
6
+ slotIndex: number
7
+ itemId: number
8
+ stats: bigint
9
+ }
10
+
11
+ export function clampUint16(value: number): number {
12
+ return Math.min(value, U16_MAX)
13
+ }
14
+
15
+ export function applySlotMultiplier(value: number, outputPct: number): number {
16
+ return clampUint16(Math.floor((value * outputPct) / 100))
17
+ }
18
+
19
+ export function getSlotAmp(layout: EntitySlot[], slotIndex: number): number {
20
+ return layout[slotIndex]?.outputPct ?? 100
21
+ }