@shipload/sdk 1.0.0-next.0 → 1.0.0-next.10

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 (51) hide show
  1. package/lib/shipload.d.ts +512 -320
  2. package/lib/shipload.js +1960 -1060
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +1920 -1056
  5. package/lib/shipload.m.js.map +1 -1
  6. package/package.json +8 -3
  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 +33 -10
  16. package/src/data/item-ids.ts +3 -1
  17. package/src/data/items.json +258 -0
  18. package/src/data/metadata.ts +57 -1
  19. package/src/data/recipes-runtime.ts +1 -0
  20. package/src/data/recipes.json +82 -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/gamestate.ts +0 -9
  28. package/src/entities/makers.ts +71 -20
  29. package/src/entities/ship-deploy.ts +114 -56
  30. package/src/entities/ship.ts +17 -0
  31. package/src/entities/slot-multiplier.ts +21 -0
  32. package/src/entities/warehouse.ts +20 -3
  33. package/src/index-module.ts +67 -26
  34. package/src/managers/actions.ts +53 -80
  35. package/src/managers/entities.ts +31 -17
  36. package/src/managers/locations.ts +2 -20
  37. package/src/nft/atomicdata.ts +125 -0
  38. package/src/nft/description.ts +41 -7
  39. package/src/nft/index.ts +1 -0
  40. package/src/resolution/resolve-item.ts +17 -9
  41. package/src/scheduling/accessor.ts +4 -0
  42. package/src/scheduling/projection.ts +8 -0
  43. package/src/scheduling/schedule.ts +15 -1
  44. package/src/scheduling/task-cargo.ts +47 -0
  45. package/src/subscriptions/connection.ts +50 -2
  46. package/src/subscriptions/manager.ts +81 -2
  47. package/src/travel/travel.ts +61 -2
  48. package/src/types/entity-traits.ts +64 -1
  49. package/src/types.ts +11 -1
  50. package/src/utils/cargo.ts +27 -0
  51. package/src/utils/system.ts +25 -24
@@ -39,6 +39,7 @@ import type {ServerContract} from '../contracts'
39
39
  import {
40
40
  ITEM_CONTAINER_T1_PACKED,
41
41
  ITEM_CONTAINER_T2_PACKED,
42
+ ITEM_EXTRACTOR_T1_PACKED,
42
43
  ITEM_SHIP_T1_PACKED,
43
44
  ITEM_WAREHOUSE_T1_PACKED,
44
45
  } from '../data/item-ids'
@@ -155,7 +156,8 @@ function resolveComponent(id: number, stats?: UInt64Type): ResolvedItem {
155
156
 
156
157
  function computeCapabilityGroup(
157
158
  moduleType: number,
158
- stats: Record<string, number>
159
+ stats: Record<string, number>,
160
+ tier: number
159
161
  ): ResolvedAttributeGroup | undefined {
160
162
  switch (moduleType) {
161
163
  case MODULE_ENGINE: {
@@ -179,7 +181,7 @@ function computeCapabilityGroup(
179
181
  }
180
182
  }
181
183
  case MODULE_GATHERER: {
182
- const caps = computeGathererCapabilities(stats)
184
+ const caps = computeGathererCapabilities(stats, tier)
183
185
  return {
184
186
  capability: 'Gatherer',
185
187
  attributes: [
@@ -223,10 +225,11 @@ function computeCapabilityGroup(
223
225
  }
224
226
  }
225
227
  case MODULE_STORAGE: {
226
- const str = stats.strength ?? 500
227
- const hrd = stats.hardness ?? 500
228
- const sat = stats.saturation ?? 500
229
- const statSum = str + hrd + sat
228
+ const str = stats.strength
229
+ const den = stats.density
230
+ const hrd = stats.hardness
231
+ const sat = stats.saturation
232
+ const statSum = str + den + hrd + sat
230
233
  const pct = 10 + Math.floor((statSum * 10) / 2997)
231
234
  return {capability: 'Storage', attributes: [{label: 'Capacity Bonus', value: pct}]}
232
235
  }
@@ -241,7 +244,7 @@ function resolveModule(id: number, stats?: UInt64Type): ResolvedItem {
241
244
  if (stats !== undefined) {
242
245
  const decoded = decodeCraftedItemStats(id, toBigStats(stats))
243
246
  const modType = getModuleCapabilityType(id)
244
- const group = computeCapabilityGroup(modType, decoded)
247
+ const group = computeCapabilityGroup(modType, decoded, item.tier)
245
248
  if (group) attributes = [group]
246
249
  }
247
250
  return {
@@ -268,6 +271,8 @@ function hullCapsForEntity(
268
271
  return computeShipHullCapabilities(decoded)
269
272
  case ITEM_WAREHOUSE_T1_PACKED:
270
273
  return computeWarehouseHullCapabilities(decoded)
274
+ case ITEM_EXTRACTOR_T1_PACKED:
275
+ return computeShipHullCapabilities(decoded)
271
276
  case ITEM_CONTAINER_T1_PACKED:
272
277
  return computeContainerCapabilities(decoded)
273
278
  case ITEM_CONTAINER_T2_PACKED:
@@ -310,13 +315,16 @@ function resolveEntity(
310
315
  const modStats = BigInt(mod.installed.stats.toString())
311
316
  const decodedStats = decodeCraftedItemStats(modItemId, modStats)
312
317
  const modType = getModuleCapabilityType(modItemId)
313
- const group = computeCapabilityGroup(modType, decodedStats)
314
318
  let modName = 'Module'
319
+ let modTier = 1
315
320
  try {
316
- modName = getItem(modItemId).name
321
+ const modItem = getItem(modItemId)
322
+ modName = modItem.name
323
+ modTier = modItem.tier
317
324
  } catch {
318
325
  modName = itemMetadata[modItemId]?.name ?? 'Module'
319
326
  }
327
+ const group = computeCapabilityGroup(modType, decodedStats, modTier)
320
328
  return {
321
329
  name: modName,
322
330
  installed: true,
@@ -72,6 +72,10 @@ export class ScheduleAccessor {
72
72
  return schedule.currentTaskProgress(this.entity, now)
73
73
  }
74
74
 
75
+ currentTaskProgressFloat(now: Date): number {
76
+ return schedule.currentTaskProgressFloat(this.entity, now)
77
+ }
78
+
75
79
  progress(now: Date): number {
76
80
  return schedule.scheduleProgress(this.entity, now)
77
81
  }
@@ -267,6 +267,10 @@ function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task):
267
267
  case TaskType.DEPLOY:
268
268
  applyDeployTask(projected, task)
269
269
  break
270
+ case TaskType.UNDEPLOY:
271
+ case TaskType.WRAP_ENTITY:
272
+ case TaskType.DEMOLISH:
273
+ break
270
274
  }
271
275
  }
272
276
 
@@ -448,6 +452,10 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
448
452
  case TaskType.DEPLOY:
449
453
  if (taskComplete) applyDeployTask(projected, task)
450
454
  break
455
+ case TaskType.UNDEPLOY:
456
+ case TaskType.WRAP_ENTITY:
457
+ case TaskType.DEMOLISH:
458
+ break
451
459
  }
452
460
  }
453
461
 
@@ -77,7 +77,7 @@ export function currentTaskIndex(entity: ScheduleData, now: Date): number {
77
77
  timeAccum += taskDuration
78
78
  }
79
79
 
80
- return entity.schedule.tasks.length - 1
80
+ return -1
81
81
  }
82
82
 
83
83
  export function currentTask(entity: ScheduleData, now: Date): Task | undefined {
@@ -147,6 +147,20 @@ export function currentTaskProgress(entity: ScheduleData, now: Date): number {
147
147
  return Math.min(1, elapsed / duration)
148
148
  }
149
149
 
150
+ export function currentTaskProgressFloat(entity: ScheduleData, now: Date): number {
151
+ if (!entity.schedule || entity.schedule.tasks.length === 0) return 0
152
+ const index = currentTaskIndex(entity, now)
153
+ if (index < 0) return 0
154
+ const task = entity.schedule.tasks[index]
155
+ const durationMs = task.duration.toNumber() * 1000
156
+ if (durationMs === 0) return 1
157
+ const startedMs = entity.schedule.started.toDate().getTime()
158
+ const taskStartMs = startedMs + getTaskStartTime(entity, index) * 1000
159
+ const elapsedMs = now.getTime() - taskStartMs
160
+ if (elapsedMs <= 0) return 0
161
+ return Math.min(1, elapsedMs / durationMs)
162
+ }
163
+
150
164
  export function scheduleProgress(entity: ScheduleData, now: Date): number {
151
165
  const duration = scheduleDuration(entity)
152
166
  if (duration === 0) return hasSchedule(entity) ? 1 : 0
@@ -0,0 +1,47 @@
1
+ import type {ServerContract} from '../contracts'
2
+ import {TaskType} from '../types'
3
+
4
+ export type TaskCargoDirection = 'in' | 'out'
5
+
6
+ export interface TaskCargoChange {
7
+ direction: TaskCargoDirection
8
+ item_id: number
9
+ stats: bigint
10
+ modules: ServerContract.Types.module_entry[]
11
+ quantity: number
12
+ }
13
+
14
+ function toChange(
15
+ item: ServerContract.Types.cargo_item,
16
+ direction: TaskCargoDirection
17
+ ): TaskCargoChange {
18
+ return {
19
+ direction,
20
+ item_id: Number(item.item_id),
21
+ stats: BigInt(item.stats.toString()),
22
+ modules: item.modules ?? [],
23
+ quantity: Number(item.quantity),
24
+ }
25
+ }
26
+
27
+ export function taskCargoChanges(task: ServerContract.Types.task): TaskCargoChange[] {
28
+ const items = task.cargo ?? []
29
+ if (items.length === 0) return []
30
+ switch (Number(task.type)) {
31
+ case TaskType.LOAD:
32
+ case TaskType.UNWRAP:
33
+ return items.map((i) => toChange(i, 'in'))
34
+ case TaskType.GATHER:
35
+ return task.entitytarget ? [] : items.map((i) => toChange(i, 'in'))
36
+ case TaskType.UNLOAD:
37
+ case TaskType.WRAP:
38
+ return items.map((i) => toChange(i, 'out'))
39
+ case TaskType.CRAFT:
40
+ return [
41
+ ...items.slice(0, -1).map((i) => toChange(i, 'out')),
42
+ toChange(items[items.length - 1], 'in'),
43
+ ]
44
+ default:
45
+ return []
46
+ }
47
+ }
@@ -7,6 +7,9 @@ export interface WebSocketConnectionOptions {
7
7
  url: string
8
8
  onMessage: (message: ServerMessage) => void
9
9
  onStateChange?: (state: ConnectionState) => void
10
+ minReconnectDelay?: number
11
+ pingIntervalMs?: number
12
+ pongTimeoutMs?: number
10
13
  }
11
14
 
12
15
  export class WebSocketConnection {
@@ -19,15 +22,26 @@ export class WebSocketConnection {
19
22
  private _state: ConnectionState = 'disconnected'
20
23
  private shouldReconnect = true
21
24
  private sendQueue: string[] = []
25
+ private minReconnectDelay: number
26
+ private pingIntervalMs: number
27
+ private pongTimeoutMs: number
28
+ private pingTimer: ReturnType<typeof setInterval> | null = null
29
+ private staleTimer: ReturnType<typeof setTimeout> | null = null
22
30
 
23
- private static readonly MIN_RECONNECT_DELAY = 1000
31
+ private static readonly DEFAULT_MIN_RECONNECT_DELAY = 1000
24
32
  private static readonly MAX_RECONNECT_DELAY = 30000
25
33
  private static readonly RECONNECT_MULTIPLIER = 2
34
+ private static readonly DEFAULT_PING_INTERVAL_MS = 25000
35
+ private static readonly DEFAULT_PONG_TIMEOUT_MS = 10000
26
36
 
27
37
  constructor(options: WebSocketConnectionOptions) {
28
38
  this.url = options.url
29
39
  this.onMessage = options.onMessage
30
40
  this.onStateChange = options.onStateChange
41
+ this.minReconnectDelay =
42
+ options.minReconnectDelay ?? WebSocketConnection.DEFAULT_MIN_RECONNECT_DELAY
43
+ this.pingIntervalMs = options.pingIntervalMs ?? WebSocketConnection.DEFAULT_PING_INTERVAL_MS
44
+ this.pongTimeoutMs = options.pongTimeoutMs ?? WebSocketConnection.DEFAULT_PONG_TIMEOUT_MS
31
45
  }
32
46
 
33
47
  get state(): ConnectionState {
@@ -64,9 +78,11 @@ export class WebSocketConnection {
64
78
  ) {
65
79
  this.ws.send(this.sendQueue.shift()!)
66
80
  }
81
+ this.startHeartbeat()
67
82
  }
68
83
 
69
84
  this.ws.onmessage = (event) => {
85
+ this.resetStaleTimer()
70
86
  try {
71
87
  const message = JSON.parse(event.data) as ServerMessage
72
88
  this.onMessage(message)
@@ -77,6 +93,7 @@ export class WebSocketConnection {
77
93
  }
78
94
 
79
95
  this.ws.onclose = () => {
96
+ this.stopHeartbeat()
80
97
  this.ws = null
81
98
  this.sendQueue.length = 0
82
99
 
@@ -104,7 +121,7 @@ export class WebSocketConnection {
104
121
  }
105
122
 
106
123
  const delay = Math.min(
107
- WebSocketConnection.MIN_RECONNECT_DELAY *
124
+ this.minReconnectDelay *
108
125
  WebSocketConnection.RECONNECT_MULTIPLIER ** this.reconnectAttempts,
109
126
  WebSocketConnection.MAX_RECONNECT_DELAY
110
127
  )
@@ -126,6 +143,8 @@ export class WebSocketConnection {
126
143
  this.reconnectTimeout = null
127
144
  }
128
145
 
146
+ this.stopHeartbeat()
147
+
129
148
  if (this.ws) {
130
149
  this.ws.close()
131
150
  this.ws = null
@@ -135,6 +154,35 @@ export class WebSocketConnection {
135
154
  this.setState('disconnected')
136
155
  }
137
156
 
157
+ private startHeartbeat() {
158
+ this.stopHeartbeat()
159
+ this.resetStaleTimer()
160
+ this.pingTimer = setInterval(() => {
161
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
162
+ this.ws.send(JSON.stringify({type: 'ping'}))
163
+ }
164
+ }, this.pingIntervalMs)
165
+ }
166
+
167
+ private stopHeartbeat() {
168
+ if (this.pingTimer) {
169
+ clearInterval(this.pingTimer)
170
+ this.pingTimer = null
171
+ }
172
+ if (this.staleTimer) {
173
+ clearTimeout(this.staleTimer)
174
+ this.staleTimer = null
175
+ }
176
+ }
177
+
178
+ private resetStaleTimer() {
179
+ if (this.staleTimer) clearTimeout(this.staleTimer)
180
+ this.staleTimer = setTimeout(() => {
181
+ debug('No frames within ping interval + pong timeout — forcing reconnect')
182
+ if (this.ws) this.ws.close()
183
+ }, this.pingIntervalMs + this.pongTimeoutMs)
184
+ }
185
+
138
186
  close() {
139
187
  this.disconnect()
140
188
  }
@@ -1,4 +1,4 @@
1
- import {WebSocketConnection} from './connection'
1
+ import {WebSocketConnection, type ConnectionState} from './connection'
2
2
  import type {
3
3
  BoundingBox,
4
4
  BoundsDeltaMessage,
@@ -22,6 +22,9 @@ export type EntityInstance = Ship | Warehouse | Container
22
22
 
23
23
  export interface SubscriptionsOptions {
24
24
  url: string
25
+ minReconnectDelay?: number
26
+ pingIntervalMs?: number
27
+ pongTimeoutMs?: number
25
28
  }
26
29
 
27
30
  export interface BoundsSubscriptionHandle {
@@ -31,6 +34,12 @@ export interface BoundsSubscriptionHandle {
31
34
  current: Map<number, EntityInstance>
32
35
  }
33
36
 
37
+ export interface OwnerSubscriptionHandle {
38
+ readonly subId: string
39
+ unsubscribe(): void
40
+ current: Map<number, EntityInstance>
41
+ }
42
+
34
43
  export interface EntitySubscriptionHandle {
35
44
  readonly subId: string
36
45
  readonly entityType: SubscriptionEntityType
@@ -53,18 +62,26 @@ export class SubscriptionsManager {
53
62
  private readonly boundsSubs = new Map<
54
63
  string,
55
64
  {
65
+ bounds?: BoundingBox
66
+ owner?: string
67
+ prioritizeOwner?: string
56
68
  onSnapshot?: (entities: EntityInstance[]) => void
57
69
  onUpdate?: (entity: EntityInstance) => void
58
70
  onBoundsDelta?: (entered: EntityInstance[], exited: number[]) => void
59
- handle: BoundsSubscriptionHandle
71
+ handle: BoundsSubscriptionHandle | OwnerSubscriptionHandle
60
72
  }
61
73
  >()
62
74
  private subCounter = 0
75
+ private hasConnected = false
63
76
 
64
77
  constructor(opts: SubscriptionsOptions) {
65
78
  this.conn = new WebSocketConnection({
66
79
  url: opts.url,
67
80
  onMessage: (m) => this.onMessage(m),
81
+ onStateChange: (s) => this.onStateChange(s),
82
+ minReconnectDelay: opts.minReconnectDelay,
83
+ pingIntervalMs: opts.pingIntervalMs,
84
+ pongTimeoutMs: opts.pongTimeoutMs,
68
85
  })
69
86
  this.conn.connect()
70
87
  }
@@ -139,6 +156,9 @@ export class SubscriptionsManager {
139
156
  current: new Map(),
140
157
  }
141
158
  this.boundsSubs.set(subId, {
159
+ bounds,
160
+ owner: handlers.owner,
161
+ prioritizeOwner: handlers.prioritizeOwner,
142
162
  onSnapshot: handlers.onSnapshot,
143
163
  onUpdate: handlers.onUpdate,
144
164
  onBoundsDelta: handlers.onBoundsDelta,
@@ -148,16 +168,75 @@ export class SubscriptionsManager {
148
168
  return handle
149
169
  }
150
170
 
171
+ subscribeOwner(
172
+ owner: string,
173
+ handlers: {
174
+ onSnapshot?: (entities: EntityInstance[]) => void
175
+ onUpdate?: (entity: EntityInstance) => void
176
+ } = {}
177
+ ): OwnerSubscriptionHandle {
178
+ const subId = this.generateSubID('own')
179
+ const msg: SubscribeMessage = {
180
+ type: 'subscribe',
181
+ sub_id: subId,
182
+ owner,
183
+ }
184
+ const handle: OwnerSubscriptionHandle = {
185
+ subId,
186
+ unsubscribe: () => this.unsubscribeBounds(subId),
187
+ current: new Map(),
188
+ }
189
+ this.boundsSubs.set(subId, {
190
+ bounds: undefined,
191
+ owner,
192
+ prioritizeOwner: undefined,
193
+ onSnapshot: handlers.onSnapshot,
194
+ onUpdate: handlers.onUpdate,
195
+ handle,
196
+ })
197
+ this.sendMessage(msg)
198
+ return handle
199
+ }
200
+
151
201
  private unsubscribeBounds(subId: string) {
152
202
  this.boundsSubs.delete(subId)
153
203
  this.sendMessage({type: 'unsubscribe', sub_id: subId})
154
204
  }
155
205
 
156
206
  private updateBounds(subId: string, bounds: BoundingBox) {
207
+ const entry = this.boundsSubs.get(subId)
208
+ if (entry) entry.bounds = bounds
157
209
  const msg: UpdateBoundsMessage = {type: 'update_bounds', sub_id: subId, bounds}
158
210
  this.sendMessage(msg)
159
211
  }
160
212
 
213
+ private onStateChange(state: ConnectionState) {
214
+ if (state !== 'connected') return
215
+ if (!this.hasConnected) {
216
+ this.hasConnected = true
217
+ return
218
+ }
219
+ for (const [subId, entry] of this.entitySubs) {
220
+ const msg: SubscribeEntityMessage = {
221
+ type: 'subscribe_entity',
222
+ sub_id: subId,
223
+ entity_type: entry.type,
224
+ entity_id: entry.id,
225
+ }
226
+ this.sendMessage(msg)
227
+ }
228
+ for (const [subId, entry] of this.boundsSubs) {
229
+ const msg: SubscribeMessage = {
230
+ type: 'subscribe',
231
+ sub_id: subId,
232
+ bounds: entry.bounds,
233
+ owner: entry.owner,
234
+ prioritize_owner: entry.prioritizeOwner,
235
+ }
236
+ this.sendMessage(msg)
237
+ }
238
+ }
239
+
161
240
  private onMessage(msg: ServerMessage) {
162
241
  switch (msg.type) {
163
242
  case 'snapshot':
@@ -28,6 +28,7 @@ import {
28
28
  type Distance,
29
29
  MAX_ORBITAL_ALTITUDE,
30
30
  MIN_ORBITAL_ALTITUDE,
31
+ MIN_TRANSFER_DISTANCE,
31
32
  PRECISION,
32
33
  type ShipLike,
33
34
  TaskType,
@@ -77,6 +78,59 @@ export function lerp(
77
78
  }
78
79
  }
79
80
 
81
+ export interface FloatPosition {
82
+ x: number
83
+ y: number
84
+ }
85
+
86
+ export function easeFlightProgress(t: number): number {
87
+ if (t <= 0) return 0
88
+ if (t >= 1) return 1
89
+ return t < 0.5 ? 2 * t * t : 1 - 2 * (1 - t) * (1 - t)
90
+ }
91
+
92
+ export function flightSpeedFactor(t: number): number {
93
+ if (t <= 0 || t >= 1) return 0
94
+ return t < 0.5 ? 4 * t : 4 * (1 - t)
95
+ }
96
+
97
+ export function interpolateFlightPosition(
98
+ origin: {x: Int64Type | number; y: Int64Type | number},
99
+ destination: {x: Int64Type | number; y: Int64Type | number},
100
+ taskProgress: number,
101
+ options?: {easing?: 'physics' | 'linear'}
102
+ ): FloatPosition {
103
+ const t = options?.easing === 'linear' ? taskProgress : easeFlightProgress(taskProgress)
104
+ return {
105
+ x: (1 - t) * Number(origin.x) + t * Number(destination.x),
106
+ y: (1 - t) * Number(origin.y) + t * Number(destination.y),
107
+ }
108
+ }
109
+
110
+ export function getInterpolatedPosition(
111
+ entity: HasScheduleAndLocation,
112
+ taskIndex: number,
113
+ taskProgress: number
114
+ ): FloatPosition {
115
+ if (!entity.schedule || entity.schedule.tasks.length === 0) {
116
+ return {x: Number(entity.coordinates.x), y: Number(entity.coordinates.y)}
117
+ }
118
+ if (taskIndex < 0) {
119
+ const settled = getFlightOrigin(entity, entity.schedule.tasks.length)
120
+ return {x: Number(settled.x), y: Number(settled.y)}
121
+ }
122
+ const task = entity.schedule.tasks[taskIndex]
123
+ if (!task.type.equals(TaskType.TRAVEL) || !task.coordinates) {
124
+ const origin = getFlightOrigin(entity, taskIndex)
125
+ return {x: Number(origin.x), y: Number(origin.y)}
126
+ }
127
+ return interpolateFlightPosition(
128
+ getFlightOrigin(entity, taskIndex),
129
+ task.coordinates,
130
+ taskProgress
131
+ )
132
+ }
133
+
80
134
  export function rotation(
81
135
  origin: ServerContract.ActionParams.Type.coordinates,
82
136
  destination: ServerContract.ActionParams.Type.coordinates
@@ -408,14 +462,18 @@ export function getDestinationLocation(
408
462
  return undefined
409
463
  }
410
464
 
465
+ /** Returns chain-tile coordinates (rounded). For visual position use getInterpolatedPosition. */
411
466
  export function getPositionAt(
412
467
  entity: HasScheduleAndLocation,
413
468
  taskIndex: number,
414
469
  taskProgress: number
415
470
  ): ServerContract.ActionParams.Type.coordinates {
416
- if (!entity.schedule || entity.schedule.tasks.length === 0 || taskIndex < 0) {
471
+ if (!entity.schedule || entity.schedule.tasks.length === 0) {
417
472
  return entity.coordinates
418
473
  }
474
+ if (taskIndex < 0) {
475
+ return getFlightOrigin(entity, entity.schedule.tasks.length)
476
+ }
419
477
 
420
478
  const task = entity.schedule.tasks[taskIndex]
421
479
 
@@ -490,7 +548,8 @@ export function calc_transfer_duration(
490
548
  : (source.location.z?.toNumber() ?? 0)
491
549
  const destZ =
492
550
  typeof dest.location.z === 'number' ? dest.location.z : (dest.location.z?.toNumber() ?? 0)
493
- const distance = Math.abs(sourceZ - destZ)
551
+ const rawDistance = Math.abs(sourceZ - destZ)
552
+ const distance = rawDistance < MIN_TRANSFER_DISTANCE ? MIN_TRANSFER_DISTANCE : rawDistance
494
553
 
495
554
  const totalMass = cargoMass + totalLoaderMass
496
555
  const acceleration = calc_acceleration(totalThrust, totalMass)
@@ -1,16 +1,60 @@
1
1
  import {Name} from '@wharfkit/antelope'
2
+ import {
3
+ ITEM_CONTAINER_T1_PACKED,
4
+ ITEM_CONTAINER_T2_PACKED,
5
+ ITEM_EXTRACTOR_T1_PACKED,
6
+ ITEM_SHIP_T1_PACKED,
7
+ ITEM_WAREHOUSE_T1_PACKED,
8
+ } from '../data/item-ids'
2
9
 
3
10
  export const ENTITY_SHIP = Name.from('ship')
4
11
  export const ENTITY_WAREHOUSE = Name.from('warehouse')
12
+ export const ENTITY_EXTRACTOR = Name.from('extractor')
5
13
  export const ENTITY_CONTAINER = Name.from('container')
6
14
 
7
- export type EntityTypeName = 'ship' | 'warehouse' | 'container'
15
+ export enum EntityClass {
16
+ OrbitalVessel = 0,
17
+ PlanetaryStructure = 1,
18
+ }
19
+
20
+ export function getEntityClass(entityType: Name | EntityTypeName): EntityClass {
21
+ const typeName = typeof entityType === 'string' ? entityType : entityType.toString()
22
+ switch (typeName) {
23
+ case 'ship':
24
+ case 'container':
25
+ return EntityClass.OrbitalVessel
26
+ case 'warehouse':
27
+ case 'extractor':
28
+ return EntityClass.PlanetaryStructure
29
+ default:
30
+ throw new Error(`Entity type has no class: ${typeName}`)
31
+ }
32
+ }
33
+
34
+ export function getPackedEntityType(itemId: number): Name | null {
35
+ switch (itemId) {
36
+ case ITEM_SHIP_T1_PACKED:
37
+ return ENTITY_SHIP
38
+ case ITEM_CONTAINER_T1_PACKED:
39
+ case ITEM_CONTAINER_T2_PACKED:
40
+ return ENTITY_CONTAINER
41
+ case ITEM_WAREHOUSE_T1_PACKED:
42
+ return ENTITY_WAREHOUSE
43
+ case ITEM_EXTRACTOR_T1_PACKED:
44
+ return ENTITY_EXTRACTOR
45
+ default:
46
+ return null
47
+ }
48
+ }
49
+
50
+ export type EntityTypeName = 'ship' | 'warehouse' | 'extractor' | 'container'
8
51
 
9
52
  export interface EntityTraits {
10
53
  typeName: Name
11
54
  isMovable: boolean
12
55
  hasEnergy: boolean
13
56
  hasLoaders: boolean
57
+ hasModules: boolean
14
58
  notFoundError: string
15
59
  }
16
60
 
@@ -19,6 +63,7 @@ export const shipTraits: EntityTraits = {
19
63
  isMovable: true,
20
64
  hasEnergy: true,
21
65
  hasLoaders: true,
66
+ hasModules: true,
22
67
 
23
68
  notFoundError: 'ship not found',
24
69
  }
@@ -28,15 +73,27 @@ export const warehouseTraits: EntityTraits = {
28
73
  isMovable: false,
29
74
  hasEnergy: false,
30
75
  hasLoaders: true,
76
+ hasModules: true,
31
77
 
32
78
  notFoundError: 'warehouse not found',
33
79
  }
34
80
 
81
+ export const extractorTraits: EntityTraits = {
82
+ typeName: ENTITY_EXTRACTOR,
83
+ isMovable: false,
84
+ hasEnergy: true,
85
+ hasLoaders: false,
86
+ hasModules: true,
87
+
88
+ notFoundError: 'extractor not found',
89
+ }
90
+
35
91
  export const containerTraits: EntityTraits = {
36
92
  typeName: ENTITY_CONTAINER,
37
93
  isMovable: true,
38
94
  hasEnergy: false,
39
95
  hasLoaders: false,
96
+ hasModules: false,
40
97
 
41
98
  notFoundError: 'container not found',
42
99
  }
@@ -49,6 +106,8 @@ export function getEntityTraits(entityType: Name | EntityTypeName): EntityTraits
49
106
  return shipTraits
50
107
  case 'warehouse':
51
108
  return warehouseTraits
109
+ case 'extractor':
110
+ return extractorTraits
52
111
  case 'container':
53
112
  return containerTraits
54
113
  default:
@@ -64,6 +123,10 @@ export function isWarehouse(entity: {type?: Name}): boolean {
64
123
  return entity.type?.equals(ENTITY_WAREHOUSE) ?? false
65
124
  }
66
125
 
126
+ export function isExtractor(entity: {type?: Name}): boolean {
127
+ return entity.type?.equals(ENTITY_EXTRACTOR) ?? false
128
+ }
129
+
67
130
  export function isContainer(entity: {type?: Name}): boolean {
68
131
  return entity.type?.equals(ENTITY_CONTAINER) ?? false
69
132
  }
package/src/types.ts CHANGED
@@ -12,7 +12,7 @@ import {ServerContract} from './contracts'
12
12
  export const PRECISION = 10000
13
13
  export const CRAFT_ENERGY_DIVISOR = 150000
14
14
 
15
- export const WAREHOUSE_Z = 500
15
+ export const PLANETARY_STRUCTURE_Z = 0
16
16
 
17
17
  export const CONTAINER_Z = 300
18
18
 
@@ -23,6 +23,8 @@ export const MAX_ORBITAL_ALTITUDE = 3000
23
23
 
24
24
  export const BASE_ORBITAL_MASS = 100000
25
25
 
26
+ export const MIN_TRANSFER_DISTANCE = 100
27
+
26
28
  export interface ShipLike {
27
29
  coordinates: ServerContract.Types.coordinates
28
30
  hullmass?: UInt32
@@ -51,6 +53,9 @@ export enum TaskType {
51
53
  DEPLOY = 8,
52
54
  WRAP = 9,
53
55
  UNWRAP = 10,
56
+ UNDEPLOY = 11,
57
+ WRAP_ENTITY = 12,
58
+ DEMOLISH = 13,
54
59
  }
55
60
 
56
61
  export enum LocationType {
@@ -58,6 +63,7 @@ export enum LocationType {
58
63
  PLANET = 1,
59
64
  ASTEROID = 2,
60
65
  NEBULA = 3,
66
+ ICE_FIELD = 4,
61
67
  }
62
68
 
63
69
  export enum TaskCancelable {
@@ -159,3 +165,7 @@ export interface Item {
159
165
  export function formatTier(tier: number): string {
160
166
  return 'T' + tier
161
167
  }
168
+
169
+ export function tierAdjective(tier: number): string {
170
+ return TIER_ADJECTIVES[tier] ?? `T${tier}`
171
+ }