@shipload/sdk 1.0.0-next.53 → 1.0.0-next.55

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.
@@ -218,6 +218,7 @@ export function computeBatteryCapabilities(stats: Record<string, number>): {
218
218
  import {
219
219
  ITEM_CONTAINER_T1_PACKED,
220
220
  ITEM_CONTAINER_T2_PACKED,
221
+ ITEM_CONSTRUCTION_DOCK_T1_PACKED,
221
222
  ITEM_EXTRACTOR_T1_PACKED,
222
223
  ITEM_FACTORY_T1_PACKED,
223
224
  ITEM_MASS_CATCHER_T1_PACKED,
@@ -277,6 +278,7 @@ export function computeBaseCapacity(itemId: number, stats: Record<string, number
277
278
  switch (itemId) {
278
279
  case ITEM_EXTRACTOR_T1_PACKED:
279
280
  case ITEM_FACTORY_T1_PACKED:
281
+ case ITEM_CONSTRUCTION_DOCK_T1_PACKED:
280
282
  case ITEM_MASS_DRIVER_T1_PACKED:
281
283
  case ITEM_MASS_CATCHER_T1_PACKED:
282
284
  case ITEM_CONTAINER_T1_PACKED:
@@ -13,6 +13,7 @@ import {
13
13
  ITEM_BEAM,
14
14
  ITEM_GATHERER_T1,
15
15
  ITEM_CRAFTER_T1,
16
+ ITEM_BUILDER_T1,
16
17
  ITEM_EXTRACTOR_T1_PACKED,
17
18
  ITEM_ROUSTABOUT_T1_PACKED,
18
19
  ITEM_PROSPECTOR_T1_PACKED,
@@ -33,7 +34,12 @@ test('getRecipeConsumers lists every recipe that consumes Sensor', () => {
33
34
  const consumers = getRecipeConsumers(ITEM_SENSOR)
34
35
  const ids = consumers.map((c) => c.outputItemId).sort((a, b) => a - b)
35
36
  expect(ids).toEqual(
36
- [ITEM_CRAFTER_T1, ITEM_EXTRACTOR_T1_PACKED, ITEM_ROUSTABOUT_T1_PACKED].sort((a, b) => a - b)
37
+ [
38
+ ITEM_CRAFTER_T1,
39
+ ITEM_BUILDER_T1,
40
+ ITEM_EXTRACTOR_T1_PACKED,
41
+ ITEM_ROUSTABOUT_T1_PACKED,
42
+ ].sort((a, b) => a - b)
37
43
  )
38
44
  })
39
45
 
@@ -31,6 +31,10 @@ export class Entity extends ServerContract.Types.entity_info {
31
31
  return schedule.isIdle(this)
32
32
  }
33
33
 
34
+ get hasPendingCapper(): boolean {
35
+ return schedule.hasPendingCapper(this)
36
+ }
37
+
34
38
  get sched(): ScheduleAccessor {
35
39
  this._sched ??= new ScheduleAccessor(this)
36
40
  return this._sched
@@ -333,6 +333,7 @@ export {
333
333
  ENTITY_WAREHOUSE,
334
334
  ENTITY_EXTRACTOR,
335
335
  ENTITY_FACTORY,
336
+ ENTITY_CONSTRUCTION_DOCK,
336
337
  ENTITY_WORKSHOP,
337
338
  ENTITY_CONTAINER,
338
339
  ENTITY_NEXUS,
@@ -351,6 +352,7 @@ export {
351
352
  isWarehouse,
352
353
  isExtractor,
353
354
  isFactory,
355
+ isConstructionDock,
354
356
  isWorkshop,
355
357
  isContainer,
356
358
  isNexus,
@@ -14,6 +14,7 @@ import {
14
14
  import {
15
15
  ITEM_CONTAINER_T1_PACKED,
16
16
  ITEM_CONTAINER_T2_PACKED,
17
+ ITEM_CONSTRUCTION_DOCK_T1_PACKED,
17
18
  ITEM_BUILDER_T1,
18
19
  ITEM_CRAFTER_T1,
19
20
  ITEM_ENGINE_T1,
@@ -37,6 +38,7 @@ import {
37
38
  ITEM_TUG_T1_PACKED,
38
39
  ITEM_PORTER_T1_PACKED,
39
40
  ITEM_WRANGLER_T1_PACKED,
41
+ ITEM_WRIGHT_T1_PACKED,
40
42
  ITEM_DREDGER_T1_PACKED,
41
43
  ITEM_WAREHOUSE_T1_PACKED,
42
44
  ITEM_WARP_T1,
@@ -159,6 +161,8 @@ export function entityDisplayName(itemId: number): string {
159
161
  return 'Porter'
160
162
  case ITEM_WRANGLER_T1_PACKED:
161
163
  return 'Wrangler'
164
+ case ITEM_WRIGHT_T1_PACKED:
165
+ return 'Wright'
162
166
  case ITEM_DREDGER_T1_PACKED:
163
167
  return 'Dredger'
164
168
  case ITEM_WAREHOUSE_T1_PACKED:
@@ -167,6 +171,8 @@ export function entityDisplayName(itemId: number): string {
167
171
  return 'Mining Rig'
168
172
  case ITEM_FACTORY_T1_PACKED:
169
173
  return 'Factory'
174
+ case ITEM_CONSTRUCTION_DOCK_T1_PACKED:
175
+ return 'Construction Dock'
170
176
  case ITEM_CONTAINER_T1_PACKED:
171
177
  return 'Container'
172
178
  case ITEM_CONTAINER_T2_PACKED:
@@ -314,6 +320,7 @@ export function buildEntityDescription(
314
320
  } else if (
315
321
  itemId === ITEM_EXTRACTOR_T1_PACKED ||
316
322
  itemId === ITEM_FACTORY_T1_PACKED ||
323
+ itemId === ITEM_CONSTRUCTION_DOCK_T1_PACKED ||
317
324
  itemId === ITEM_CONTAINER_T1_PACKED ||
318
325
  itemId === ITEM_CONTAINER_T2_PACKED
319
326
  ) {
@@ -0,0 +1,119 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {ServerContract, TaskType} from '../index-module'
3
+ import {hasPendingCapper, hasResolvable, isCapperTaskType} from './schedule'
4
+
5
+ const T0 = '2026-06-19T00:00:00'
6
+ const NOW = new Date('2026-06-19T00:01:00.000Z')
7
+
8
+ function task(over: Partial<{type: number; duration: number}>) {
9
+ return ServerContract.Types.task.from({
10
+ type: over.type ?? TaskType.TRAVEL,
11
+ duration: over.duration ?? 30,
12
+ cancelable: 0,
13
+ cargo: [],
14
+ couplings: [],
15
+ })
16
+ }
17
+
18
+ function entity(
19
+ tasks: ReturnType<typeof task>[],
20
+ holds: ServerContract.Types.hold[] = [],
21
+ startedISO = T0
22
+ ) {
23
+ return ServerContract.Types.entity_info.from({
24
+ type: 'ship',
25
+ id: 1,
26
+ owner: 'player.gm',
27
+ entity_name: 'Ship 1',
28
+ coordinates: {x: 0, y: 0, z: 0},
29
+ item_id: 1,
30
+ cargomass: 0,
31
+ cargo: [],
32
+ modules: [],
33
+ lanes: [{lane_key: 0, schedule: {started: startedISO, tasks}}],
34
+ gatherer_lanes: [],
35
+ crafter_lanes: [],
36
+ builder_lanes: [],
37
+ loader_lanes: [],
38
+ holds,
39
+ })
40
+ }
41
+
42
+ function hold(kind: number) {
43
+ return ServerContract.Types.hold.from({
44
+ id: 1,
45
+ kind,
46
+ counterpart: {entity_type: 'ship', entity_id: 2},
47
+ until: T0,
48
+ incoming_mass: 0,
49
+ })
50
+ }
51
+
52
+ describe('isCapperTaskType', () => {
53
+ test('UNDEPLOY, DEMOLISH are cappers', () => {
54
+ expect(isCapperTaskType(TaskType.UNDEPLOY)).toBe(true)
55
+ expect(isCapperTaskType(TaskType.DEMOLISH)).toBe(true)
56
+ })
57
+
58
+ test('REFIT is not a capper', () => {
59
+ expect(isCapperTaskType(TaskType.REFIT)).toBe(false)
60
+ })
61
+
62
+ test('non-capper task types are not cappers', () => {
63
+ expect(isCapperTaskType(TaskType.TRAVEL)).toBe(false)
64
+ expect(isCapperTaskType(TaskType.LOAD)).toBe(false)
65
+ })
66
+ })
67
+
68
+ describe('hasPendingCapper', () => {
69
+ test('false with no schedule', () => {
70
+ expect(hasPendingCapper(entity([]))).toBe(false)
71
+ })
72
+
73
+ test('false with only non-capper tasks queued', () => {
74
+ expect(hasPendingCapper(entity([task({type: TaskType.TRAVEL})]))).toBe(false)
75
+ })
76
+
77
+ test('true when a DEMOLISH task is queued anywhere in the lane', () => {
78
+ const e = entity([task({type: TaskType.TRAVEL}), task({type: TaskType.DEMOLISH})])
79
+ expect(hasPendingCapper(e)).toBe(true)
80
+ })
81
+
82
+ test('true when a DEMOLISH task is queued', () => {
83
+ expect(hasPendingCapper(entity([task({type: TaskType.DEMOLISH})]))).toBe(true)
84
+ })
85
+ })
86
+
87
+ describe('hasResolvable — capper gating', () => {
88
+ test('a completed non-capper front is resolvable', () => {
89
+ const e = entity([task({type: TaskType.TRAVEL, duration: 30})])
90
+ expect(hasResolvable(e, NOW)).toBe(true)
91
+ })
92
+
93
+ test('a completed capper front with no holds is resolvable', () => {
94
+ const e = entity([task({type: TaskType.DEMOLISH, duration: 30})])
95
+ expect(hasResolvable(e, NOW)).toBe(true)
96
+ })
97
+
98
+ test('a completed capper front with a live hold is gated', () => {
99
+ const e = entity([task({type: TaskType.DEMOLISH, duration: 30})], [hold(2)])
100
+ expect(hasResolvable(e, NOW)).toBe(false)
101
+ })
102
+
103
+ test('a capper front clears once holds empty', () => {
104
+ const withHold = entity([task({type: TaskType.DEMOLISH, duration: 30})], [hold(2)])
105
+ const cleared = entity([task({type: TaskType.DEMOLISH, duration: 30})], [])
106
+ expect(hasResolvable(withHold, NOW)).toBe(false)
107
+ expect(hasResolvable(cleared, NOW)).toBe(true)
108
+ })
109
+
110
+ test('a live hold does not gate a non-capper front', () => {
111
+ const e = entity([task({type: TaskType.TRAVEL, duration: 30})], [hold(2)])
112
+ expect(hasResolvable(e, NOW)).toBe(true)
113
+ })
114
+
115
+ test('an in-progress front is not resolvable regardless of holds', () => {
116
+ const e = entity([task({type: TaskType.DEMOLISH, duration: 3600})])
117
+ expect(hasResolvable(e, NOW)).toBe(false)
118
+ })
119
+ })
@@ -63,6 +63,20 @@ export function isIdle(entity: ScheduleData): boolean {
63
63
  return !hasSchedule(entity) && !hasHolds(entity)
64
64
  }
65
65
 
66
+ // Mirrors is_capper_task_type: demolish/undeploy cap a plan — no further appends once queued.
67
+ export function isCapperTaskType(taskType: number): boolean {
68
+ return taskType === TaskType.UNDEPLOY || taskType === TaskType.DEMOLISH
69
+ }
70
+
71
+ export function hasPendingCapper(entity: ScheduleData): boolean {
72
+ for (const l of entity.lanes ?? []) {
73
+ for (const t of l.schedule.tasks) {
74
+ if (isCapperTaskType(t.type.toNumber())) return true
75
+ }
76
+ }
77
+ return false
78
+ }
79
+
66
80
  export function isEntityIdle(entity: ScheduleData, now: Date): boolean {
67
81
  if (hasHolds(entity)) return false
68
82
  const lanes = entity.lanes
@@ -121,10 +135,13 @@ export function scheduleComplete(entity: ScheduleData, now: Date): boolean {
121
135
  return remaining === 0
122
136
  }
123
137
 
124
- // Mirrors contract lane_front_complete: any lane whose front task is complete and non-reserved.
138
+ // Mirrors lane_front_complete && !capper_front_gated for own-entity holds (workshop/undeploy-target gates need cross-entity context this ScheduleData lacks).
125
139
  export function hasResolvable(entity: ScheduleData, now: Date): boolean {
126
140
  for (const l of entity.lanes ?? []) {
127
- if (core.laneTaskComplete(l.schedule, 0, now)) return true
141
+ if (!core.laneTaskComplete(l.schedule, 0, now)) continue
142
+ const front = l.schedule.tasks[0]
143
+ if (isCapperTaskType(front.type.toNumber()) && hasHolds(entity)) continue
144
+ return true
128
145
  }
129
146
  return false
130
147
  }
package/src/types.ts CHANGED
@@ -69,6 +69,13 @@ export enum TaskType {
69
69
  CHARGE = 16,
70
70
  UPGRADE = 17,
71
71
  SHUTTLE = 18,
72
+ REFIT = 19,
73
+ }
74
+
75
+ export enum RefitOp {
76
+ ADD = 0,
77
+ REMOVE = 1,
78
+ SWAP = 2,
72
79
  }
73
80
 
74
81
  export enum HoldKind {