@shipload/sdk 1.0.0-next.5 → 1.0.0-next.50

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 (144) 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 +2881 -1149
  7. package/lib/shipload.js +13031 -5511
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +12807 -5460
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1028 -0
  12. package/lib/testing.js +4336 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +4330 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.ts +51 -0
  18. package/src/capabilities/crafting.test.ts +7 -0
  19. package/src/capabilities/crafting.ts +6 -7
  20. package/src/capabilities/gathering.test.ts +29 -0
  21. package/src/capabilities/gathering.ts +37 -19
  22. package/src/capabilities/index.ts +0 -1
  23. package/src/capabilities/modules.ts +22 -0
  24. package/src/capabilities/storage.ts +16 -1
  25. package/src/contracts/platform.ts +231 -3
  26. package/src/contracts/server.ts +1160 -520
  27. package/src/coordinates/address.ts +88 -0
  28. package/src/coordinates/constants.test.ts +15 -0
  29. package/src/coordinates/constants.ts +23 -0
  30. package/src/coordinates/index.ts +15 -0
  31. package/src/coordinates/memo.test.ts +47 -0
  32. package/src/coordinates/memo.ts +20 -0
  33. package/src/coordinates/permutation.ts +77 -0
  34. package/src/coordinates/regions.ts +48 -0
  35. package/src/coordinates/sectors.ts +115 -0
  36. package/src/data/capabilities.ts +31 -16
  37. package/src/data/capability-formulas.ts +21 -14
  38. package/src/data/catalog.ts +0 -5
  39. package/src/data/colors.ts +13 -47
  40. package/src/data/entities.json +154 -10
  41. package/src/data/item-ids.ts +31 -12
  42. package/src/data/items.json +192 -74
  43. package/src/data/kind-registry.json +131 -0
  44. package/src/data/kind-registry.ts +172 -0
  45. package/src/data/metadata.ts +166 -48
  46. package/src/data/recipes-runtime.ts +5 -23
  47. package/src/data/recipes.json +1003 -163
  48. package/src/derivation/build-methods.test.ts +13 -0
  49. package/src/derivation/build-methods.ts +48 -0
  50. package/src/derivation/capabilities.test.ts +151 -0
  51. package/src/derivation/capabilities.ts +527 -0
  52. package/src/derivation/capability-mappings.ts +58 -16
  53. package/src/derivation/crafting.ts +36 -33
  54. package/src/derivation/index.ts +28 -2
  55. package/src/derivation/recipe-usage.test.ts +81 -0
  56. package/src/derivation/recipe-usage.ts +141 -0
  57. package/src/derivation/reserve-regen.ts +34 -0
  58. package/src/derivation/resources.ts +98 -19
  59. package/src/derivation/rollups.test.ts +55 -0
  60. package/src/derivation/rollups.ts +56 -0
  61. package/src/derivation/stars.test.ts +67 -0
  62. package/src/derivation/stars.ts +25 -0
  63. package/src/derivation/stats.ts +6 -6
  64. package/src/derivation/stratum.ts +26 -22
  65. package/src/derivation/tiers.ts +40 -7
  66. package/src/derivation/upgrades.ts +14 -0
  67. package/src/derivation/wormhole.ts +141 -0
  68. package/src/entities/entity.ts +98 -0
  69. package/src/entities/gamestate.ts +3 -28
  70. package/src/entities/makers.ts +131 -134
  71. package/src/entities/slot-multiplier.ts +43 -0
  72. package/src/errors.ts +12 -16
  73. package/src/format.ts +26 -4
  74. package/src/index-module.ts +290 -34
  75. package/src/managers/actions.ts +602 -105
  76. package/src/managers/base.ts +6 -2
  77. package/src/managers/cluster.test.ts +39 -0
  78. package/src/managers/cluster.ts +53 -0
  79. package/src/managers/construction-types.ts +80 -0
  80. package/src/managers/construction.ts +412 -0
  81. package/src/managers/context.ts +29 -1
  82. package/src/managers/coordinates.ts +14 -0
  83. package/src/managers/entities.ts +27 -66
  84. package/src/managers/epochs.ts +40 -0
  85. package/src/managers/flatten-gather-plan.test.ts +80 -0
  86. package/src/managers/index.ts +20 -1
  87. package/src/managers/locations.ts +25 -29
  88. package/src/managers/nft.test.ts +14 -0
  89. package/src/managers/nft.ts +70 -0
  90. package/src/managers/players.ts +25 -0
  91. package/src/managers/plot.ts +122 -0
  92. package/src/nft/atomicassets.abi.json +1342 -0
  93. package/src/nft/atomicassets.ts +237 -0
  94. package/src/nft/atomicdata.ts +130 -0
  95. package/src/nft/buildImmutableData.ts +336 -0
  96. package/src/nft/description.ts +104 -36
  97. package/src/nft/index.ts +3 -0
  98. package/src/planner/index.ts +242 -0
  99. package/src/planner/planner.test.ts +325 -0
  100. package/src/resolution/describe-module.ts +43 -25
  101. package/src/resolution/display-name.ts +38 -10
  102. package/src/resolution/resolve-item.test.ts +48 -0
  103. package/src/resolution/resolve-item.ts +74 -47
  104. package/src/scan/index.ts +244 -0
  105. package/src/scan/scan-wasm.base64.ts +2 -0
  106. package/src/scheduling/accessor.ts +65 -23
  107. package/src/scheduling/availability.ts +108 -0
  108. package/src/scheduling/cancel.test.ts +348 -0
  109. package/src/scheduling/cancel.ts +209 -0
  110. package/src/scheduling/energy.ts +47 -0
  111. package/src/scheduling/idle-resolve.ts +45 -0
  112. package/src/scheduling/lane-core.ts +128 -0
  113. package/src/scheduling/lanes.test.ts +249 -0
  114. package/src/scheduling/lanes.ts +198 -0
  115. package/src/scheduling/projection.ts +210 -107
  116. package/src/scheduling/schedule.ts +240 -117
  117. package/src/scheduling/task-cargo.ts +46 -0
  118. package/src/scheduling/unwrap.test.ts +86 -0
  119. package/src/scheduling/unwrap.ts +190 -0
  120. package/src/shipload.ts +26 -1
  121. package/src/subscriptions/manager.cluster.test.ts +51 -0
  122. package/src/subscriptions/manager.ts +245 -172
  123. package/src/subscriptions/mappers.ts +5 -8
  124. package/src/subscriptions/types.ts +28 -3
  125. package/src/testing/catalog-hash.ts +19 -0
  126. package/src/testing/index.ts +2 -0
  127. package/src/testing/projection-parity.ts +167 -0
  128. package/src/travel/reach.ts +23 -0
  129. package/src/travel/route-planner.ts +277 -0
  130. package/src/travel/travel.ts +148 -117
  131. package/src/types/capabilities.ts +29 -6
  132. package/src/types/entity.ts +3 -3
  133. package/src/types/index.ts +0 -1
  134. package/src/types.ts +36 -14
  135. package/src/utils/cargo.test.ts +14 -0
  136. package/src/utils/cargo.ts +29 -0
  137. package/src/utils/display-name.ts +70 -0
  138. package/src/utils/system.ts +36 -24
  139. package/src/capabilities/loading.ts +0 -8
  140. package/src/entities/container.ts +0 -123
  141. package/src/entities/ship-deploy.ts +0 -295
  142. package/src/entities/ship.ts +0 -221
  143. package/src/entities/warehouse.ts +0 -127
  144. package/src/types/entity-traits.ts +0 -69
@@ -1,193 +1,316 @@
1
1
  import type {ServerContract} from '../contracts'
2
2
  import {TaskType} from '../types'
3
+ import * as core from './lane-core'
3
4
 
4
5
  type Schedule = ServerContract.Types.schedule
5
6
  type Task = ServerContract.Types.task
7
+ type Lane = ServerContract.Types.lane
8
+ type Hold = ServerContract.Types.hold
9
+
10
+ export const LANE_MOBILITY = 0
11
+ export const LANE_BARRIER = 255
6
12
 
7
13
  export interface ScheduleData {
8
- schedule?: Schedule
9
- }
10
-
11
- export interface Scheduleable extends ScheduleData {
12
- hasSchedule: boolean
13
- isIdle: boolean
14
- tasks: Task[]
15
- scheduleDuration(): number
16
- scheduleElapsed(now: Date): number
17
- scheduleRemaining(now: Date): number
18
- scheduleComplete(now: Date): boolean
19
- currentTaskIndex(now: Date): number
20
- currentTask(now: Date): Task | undefined
21
- currentTaskType(now: Date): TaskType | undefined
22
- getTaskStartTime(index: number): number
23
- getTaskElapsed(index: number, now: Date): number
24
- getTaskRemaining(index: number, now: Date): number
25
- isTaskComplete(index: number, now: Date): boolean
26
- isTaskInProgress(index: number, now: Date): boolean
27
- currentTaskProgress(now: Date): number
28
- scheduleProgress(now: Date): number
14
+ lanes?: Lane[]
15
+ holds?: Hold[]
16
+ }
17
+
18
+ export interface LaneView {
19
+ laneKey: number
20
+ schedule: Schedule
21
+ }
22
+
23
+ export {
24
+ laneStartsIn,
25
+ currentTaskIndexForLane,
26
+ laneTaskComplete,
27
+ laneTaskInProgress,
28
+ laneCompletesAt,
29
+ currentTaskProgressFloatForLane,
30
+ } from './lane-core'
31
+
32
+ export function getLanes(entity: ScheduleData): LaneView[] {
33
+ const lanes = entity.lanes
34
+ if (!lanes || lanes.length === 0) return []
35
+ return lanes.map((l) => ({laneKey: l.lane_key.toNumber(), schedule: l.schedule}))
36
+ }
37
+
38
+ export function getLane(entity: ScheduleData, laneKey: number): LaneView | undefined {
39
+ const lanes = entity.lanes
40
+ if (!lanes) return undefined
41
+ for (const l of lanes) {
42
+ if (l.lane_key.toNumber() === laneKey) return {laneKey, schedule: l.schedule}
43
+ }
44
+ return undefined
45
+ }
46
+
47
+ export function mobilityLane(entity: ScheduleData): LaneView | undefined {
48
+ return getLane(entity, LANE_MOBILITY)
29
49
  }
30
50
 
31
51
  export function hasSchedule(entity: ScheduleData): boolean {
32
- return !!entity.schedule && entity.schedule.tasks.length > 0
52
+ const lanes = entity.lanes
53
+ if (!lanes) return false
54
+ return lanes.some((l) => l.schedule.tasks.length > 0)
55
+ }
56
+
57
+ export function hasHolds(entity: ScheduleData): boolean {
58
+ const holds = entity.holds
59
+ return !!holds && holds.length > 0
33
60
  }
34
61
 
35
62
  export function isIdle(entity: ScheduleData): boolean {
36
- return !hasSchedule(entity)
63
+ return !hasSchedule(entity) && !hasHolds(entity)
64
+ }
65
+
66
+ export function isEntityIdle(entity: ScheduleData, now: Date): boolean {
67
+ if (hasHolds(entity)) return false
68
+ const lanes = entity.lanes
69
+ if (!lanes) return true
70
+ return lanes.every((l) => core.currentTaskIndexForLane(l.schedule, now) < 0)
71
+ }
72
+
73
+ export function entityIdleAt(entity: ScheduleData, _now: Date): Date | undefined {
74
+ const lanes = entity.lanes
75
+ if (!lanes) return undefined
76
+ let maxMs: number | undefined
77
+ for (const l of lanes) {
78
+ if (l.schedule.tasks.length === 0) continue
79
+ const endMs = l.schedule.started.toDate().getTime() + core.laneDuration(l.schedule) * 1000
80
+ if (maxMs === undefined || endMs > maxMs) maxMs = endMs
81
+ }
82
+ return maxMs === undefined ? undefined : new Date(maxMs)
37
83
  }
38
84
 
39
85
  export function getTasks(entity: ScheduleData): Task[] {
40
- return entity.schedule?.tasks || []
86
+ const lanes = entity.lanes
87
+ if (!lanes) return []
88
+ return lanes.flatMap((l) => l.schedule.tasks)
41
89
  }
42
90
 
43
91
  export function scheduleDuration(entity: ScheduleData): number {
44
- if (!entity.schedule) return 0
45
- return entity.schedule.tasks.reduce((sum, task) => sum + task.duration.toNumber(), 0)
92
+ let max = 0
93
+ for (const l of entity.lanes ?? []) max = Math.max(max, core.laneDuration(l.schedule))
94
+ return max
46
95
  }
47
96
 
48
97
  export function scheduleElapsed(entity: ScheduleData, now: Date): number {
49
- if (!entity.schedule) return 0
50
- const started = entity.schedule.started.toDate()
51
- const elapsed = Math.floor((now.getTime() - started.getTime()) / 1000)
52
- return Math.max(0, elapsed)
98
+ let max = 0
99
+ for (const l of entity.lanes ?? []) max = Math.max(max, core.laneElapsed(l.schedule, now))
100
+ return max
53
101
  }
54
102
 
55
103
  export function scheduleRemaining(entity: ScheduleData, now: Date): number {
56
- if (!entity.schedule) return 0
57
- const duration = scheduleDuration(entity)
58
- const elapsed = scheduleElapsed(entity, now)
59
- return Math.max(0, duration - elapsed)
104
+ let remaining = 0
105
+ for (const l of entity.lanes ?? []) {
106
+ remaining = Math.max(remaining, core.laneRemaining(l.schedule, now))
107
+ }
108
+ return remaining
60
109
  }
61
110
 
62
111
  export function scheduleComplete(entity: ScheduleData, now: Date): boolean {
63
- return hasSchedule(entity) && scheduleRemaining(entity, now) === 0
112
+ const lanes = entity.lanes
113
+ if (!lanes) return false
114
+ let hasAnyTask = false
115
+ let remaining = 0
116
+ for (const l of lanes) {
117
+ if (l.schedule.tasks.length > 0) hasAnyTask = true
118
+ remaining = Math.max(remaining, core.laneRemaining(l.schedule, now))
119
+ }
120
+ if (!hasAnyTask) return false
121
+ return remaining === 0
64
122
  }
65
123
 
66
- export function currentTaskIndex(entity: ScheduleData, now: Date): number {
67
- if (!entity.schedule || entity.schedule.tasks.length === 0) return -1
68
-
69
- const elapsed = scheduleElapsed(entity, now)
70
- let timeAccum = 0
71
-
72
- for (let i = 0; i < entity.schedule.tasks.length; i++) {
73
- const taskDuration = entity.schedule.tasks[i].duration.toNumber()
74
- if (elapsed < timeAccum + taskDuration) {
75
- return i
76
- }
77
- timeAccum += taskDuration
124
+ // Mirrors contract lane_front_complete: any lane whose front task is complete and non-reserved.
125
+ export function hasResolvable(entity: ScheduleData, now: Date): boolean {
126
+ for (const l of entity.lanes ?? []) {
127
+ if (core.laneTaskComplete(l.schedule, 0, now)) return true
78
128
  }
129
+ return false
130
+ }
79
131
 
80
- return -1
132
+ export function currentTaskForLane(
133
+ entity: ScheduleData,
134
+ laneKey: number,
135
+ now: Date
136
+ ): Task | undefined {
137
+ const lane = getLane(entity, laneKey)
138
+ return lane ? core.currentTask(lane.schedule, now) : undefined
81
139
  }
82
140
 
83
- export function currentTask(entity: ScheduleData, now: Date): Task | undefined {
84
- const index = currentTaskIndex(entity, now)
85
- if (index < 0 || !entity.schedule) return undefined
86
- return entity.schedule.tasks[index]
141
+ export function currentTaskTypeForLane(
142
+ entity: ScheduleData,
143
+ laneKey: number,
144
+ now: Date
145
+ ): TaskType | undefined {
146
+ const lane = getLane(entity, laneKey)
147
+ return lane ? core.currentTaskType(lane.schedule, now) : undefined
87
148
  }
88
149
 
89
- export function currentTaskType(entity: ScheduleData, now: Date): TaskType | undefined {
90
- const task = currentTask(entity, now)
91
- if (!task) return undefined
92
- return task.type.toNumber() as TaskType
150
+ export function activeTasks(entity: ScheduleData, now: Date): Task[] {
151
+ const out: Task[] = []
152
+ for (const l of entity.lanes ?? []) {
153
+ const idx = core.currentTaskIndexForLane(l.schedule, now)
154
+ if (idx >= 0) out.push(l.schedule.tasks[idx])
155
+ }
156
+ return out
157
+ }
158
+
159
+ export interface ResolvedEvent {
160
+ laneKey: number
161
+ taskIndex: number
162
+ task: Task
163
+ completesAt: Date
93
164
  }
94
165
 
95
- export function getTaskStartTime(entity: ScheduleData, index: number): number {
96
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return 0
97
- let timeAccum = 0
98
- for (let i = 0; i < index; i++) {
99
- timeAccum += entity.schedule.tasks[i].duration.toNumber()
166
+ // Canonical lane-front order (mirrors contract front_precedes): completion, then RECHARGE-last, then lane key.
167
+ function frontPrecedes(
168
+ a: {completesAt: Date; task: Task; laneKey: number},
169
+ b: {completesAt: Date; task: Task; laneKey: number}
170
+ ): number {
171
+ if (a.completesAt.getTime() !== b.completesAt.getTime()) {
172
+ return a.completesAt.getTime() - b.completesAt.getTime()
100
173
  }
101
- return timeAccum
174
+ const aRecharge = a.task.type.toNumber() === TaskType.RECHARGE
175
+ const bRecharge = b.task.type.toNumber() === TaskType.RECHARGE
176
+ if (aRecharge !== bRecharge) return aRecharge ? 1 : -1
177
+ return a.laneKey - b.laneKey
102
178
  }
103
179
 
104
- export function getTaskElapsed(entity: ScheduleData, index: number, now: Date): number {
105
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return 0
180
+ // Completed lane-fronts in canonical order (mirrors contract front_precedes).
181
+ export function resolveOrder(entity: ScheduleData, now: Date): ResolvedEvent[] {
182
+ const events: ResolvedEvent[] = []
183
+ for (const l of entity.lanes ?? []) {
184
+ const laneKey = l.lane_key.toNumber()
185
+ const startedMs = l.schedule.started.toDate().getTime()
186
+ let endSec = 0
187
+ for (let i = 0; i < l.schedule.tasks.length; i++) {
188
+ const task = l.schedule.tasks[i]
189
+ endSec += task.duration.toNumber()
190
+ const completesAt = new Date(startedMs + endSec * 1000)
191
+ if (completesAt.getTime() > now.getTime()) break
192
+ events.push({laneKey, taskIndex: i, task, completesAt})
193
+ }
194
+ }
195
+ events.sort(frontPrecedes)
196
+ return events
197
+ }
106
198
 
107
- const elapsed = scheduleElapsed(entity, now)
108
- const taskStart = getTaskStartTime(entity, index)
109
- const taskDuration = entity.schedule.tasks[index].duration.toNumber()
199
+ export interface OrderedTask {
200
+ laneKey: number
201
+ taskIndex: number
202
+ task: Task
203
+ startsAt: Date
204
+ completesAt: Date
205
+ }
110
206
 
111
- if (elapsed <= taskStart) return 0
112
- const elapsedInTask = elapsed - taskStart
113
- return Math.min(elapsedInTask, taskDuration)
207
+ // Every task across all lanes in canonical order (mirrors contract front_precedes).
208
+ export function orderedTasks(entity: ScheduleData): OrderedTask[] {
209
+ const out: OrderedTask[] = []
210
+ for (const l of entity.lanes ?? []) {
211
+ const laneKey = l.lane_key.toNumber()
212
+ const startedMs = l.schedule.started.toDate().getTime()
213
+ let endSec = 0
214
+ for (let i = 0; i < l.schedule.tasks.length; i++) {
215
+ const task = l.schedule.tasks[i]
216
+ const startsAt = new Date(startedMs + endSec * 1000)
217
+ endSec += task.duration.toNumber()
218
+ const completesAt = new Date(startedMs + endSec * 1000)
219
+ out.push({laneKey, taskIndex: i, task, startsAt, completesAt})
220
+ }
221
+ }
222
+ out.sort(frontPrecedes)
223
+ return out
114
224
  }
115
225
 
116
- export function getTaskRemaining(entity: ScheduleData, index: number, now: Date): number {
117
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return 0
226
+ export function laneRemainingOf(entity: ScheduleData, laneKey: number, now: Date): number {
227
+ const lane = getLane(entity, laneKey)
228
+ return lane ? core.laneRemaining(lane.schedule, now) : 0
229
+ }
118
230
 
119
- const taskDuration = entity.schedule.tasks[index].duration.toNumber()
120
- const taskElapsed = getTaskElapsed(entity, index, now)
121
- return Math.max(0, taskDuration - taskElapsed)
231
+ export function laneStartsInOf(entity: ScheduleData, laneKey: number, now: Date): number {
232
+ const lane = getLane(entity, laneKey)
233
+ return lane ? core.laneStartsIn(lane.schedule, now) : 0
122
234
  }
123
235
 
124
- export function isTaskComplete(entity: ScheduleData, index: number, now: Date): boolean {
125
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return false
236
+ export function laneCompleteOf(entity: ScheduleData, laneKey: number, now: Date): boolean {
237
+ const lane = getLane(entity, laneKey)
238
+ return lane ? core.laneComplete(lane.schedule, now) : false
239
+ }
126
240
 
127
- const taskDuration = entity.schedule.tasks[index].duration.toNumber()
128
- const taskElapsed = getTaskElapsed(entity, index, now)
129
- return taskElapsed >= taskDuration
241
+ export function laneProgressOf(entity: ScheduleData, laneKey: number, now: Date): number {
242
+ const lane = getLane(entity, laneKey)
243
+ return lane ? core.laneProgress(lane.schedule, now) : 0
130
244
  }
131
245
 
132
- export function isTaskInProgress(entity: ScheduleData, index: number, now: Date): boolean {
133
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return false
246
+ export function laneTaskElapsedOf(
247
+ entity: ScheduleData,
248
+ laneKey: number,
249
+ index: number,
250
+ now: Date
251
+ ): number {
252
+ const lane = getLane(entity, laneKey)
253
+ return lane ? core.laneTaskElapsed(lane.schedule, index, now) : 0
254
+ }
134
255
 
135
- const taskElapsed = getTaskElapsed(entity, index, now)
136
- const taskDuration = entity.schedule.tasks[index].duration.toNumber()
137
- return taskElapsed > 0 && taskElapsed < taskDuration
256
+ export function laneTaskRemainingOf(
257
+ entity: ScheduleData,
258
+ laneKey: number,
259
+ index: number,
260
+ now: Date
261
+ ): number {
262
+ const lane = getLane(entity, laneKey)
263
+ return lane ? core.laneTaskRemaining(lane.schedule, index, now) : 0
138
264
  }
139
265
 
140
- export function currentTaskProgress(entity: ScheduleData, now: Date): number {
141
- const task = currentTask(entity, now)
142
- if (!task) return 0
143
- const index = currentTaskIndex(entity, now)
144
- const elapsed = getTaskElapsed(entity, index, now)
145
- const duration = task.duration.toNumber()
146
- if (duration === 0) return 1
147
- return Math.min(1, elapsed / duration)
266
+ export function laneTaskCompleteOf(
267
+ entity: ScheduleData,
268
+ laneKey: number,
269
+ index: number,
270
+ now: Date
271
+ ): boolean {
272
+ const lane = getLane(entity, laneKey)
273
+ return lane ? core.laneTaskComplete(lane.schedule, index, now) : false
148
274
  }
149
275
 
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)
276
+ export function laneTaskInProgressOf(
277
+ entity: ScheduleData,
278
+ laneKey: number,
279
+ index: number,
280
+ now: Date
281
+ ): boolean {
282
+ const lane = getLane(entity, laneKey)
283
+ return lane ? core.laneTaskInProgress(lane.schedule, index, now) : false
162
284
  }
163
285
 
164
- export function scheduleProgress(entity: ScheduleData, now: Date): number {
165
- const duration = scheduleDuration(entity)
166
- if (duration === 0) return hasSchedule(entity) ? 1 : 0
167
- const elapsed = scheduleElapsed(entity, now)
168
- return Math.min(1, elapsed / duration)
286
+ export function currentTaskIndexOf(entity: ScheduleData, laneKey: number, now: Date): number {
287
+ const lane = getLane(entity, laneKey)
288
+ return lane ? core.currentTaskIndexForLane(lane.schedule, now) : -1
169
289
  }
170
290
 
171
- export function isTaskType(entity: ScheduleData, taskType: TaskType, now: Date): boolean {
172
- return currentTaskType(entity, now) === taskType
291
+ function entityDoesTaskType(entity: ScheduleData, taskType: TaskType, now: Date): boolean {
292
+ return activeTasks(entity, now).some((t) => t.type.toNumber() === taskType)
173
293
  }
174
294
 
175
295
  export function isInFlight(entity: ScheduleData, now: Date): boolean {
176
- return isTaskType(entity, TaskType.TRAVEL, now)
296
+ const lane = mobilityLane(entity)
297
+ if (!lane) return false
298
+ const t = core.currentTaskType(lane.schedule, now)
299
+ return t === TaskType.TRAVEL || t === TaskType.TRANSIT
177
300
  }
178
301
 
179
302
  export function isRecharging(entity: ScheduleData, now: Date): boolean {
180
- return isTaskType(entity, TaskType.RECHARGE, now)
303
+ return entityDoesTaskType(entity, TaskType.RECHARGE, now)
181
304
  }
182
305
 
183
306
  export function isLoading(entity: ScheduleData, now: Date): boolean {
184
- return isTaskType(entity, TaskType.LOAD, now)
307
+ return entityDoesTaskType(entity, TaskType.LOAD, now)
185
308
  }
186
309
 
187
310
  export function isUnloading(entity: ScheduleData, now: Date): boolean {
188
- return isTaskType(entity, TaskType.UNLOAD, now)
311
+ return entityDoesTaskType(entity, TaskType.UNLOAD, now)
189
312
  }
190
313
 
191
314
  export function isGathering(entity: ScheduleData, now: Date): boolean {
192
- return isTaskType(entity, TaskType.GATHER, now)
315
+ return entityDoesTaskType(entity, TaskType.GATHER, now)
193
316
  }
@@ -0,0 +1,46 @@
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
+ return items.map((i) => toChange(i, 'out'))
38
+ case TaskType.CRAFT:
39
+ return [
40
+ ...items.slice(0, -1).map((i) => toChange(i, 'out')),
41
+ toChange(items[items.length - 1], 'in'),
42
+ ]
43
+ default:
44
+ return []
45
+ }
46
+ }
@@ -0,0 +1,86 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {
3
+ derivedLoaders,
4
+ unwrapTransitDuration,
5
+ unwrapLoadDuration,
6
+ estimateUnwrapDuration,
7
+ incomingHoldMass,
8
+ projectedPeakCargomass,
9
+ } from './unwrap'
10
+
11
+ describe('unwrap duration mirror', () => {
12
+ test('derivedLoaders aggregates lanes like derived_loaders()', () => {
13
+ expect(derivedLoaders([])).toBeNull()
14
+ expect(
15
+ derivedLoaders([
16
+ {mass: 1000, thrust: 10},
17
+ {mass: 1400, thrust: 20},
18
+ ])
19
+ ).toEqual({mass: 1200, thrust: 30, quantity: 2}) // floor(2400/2)=1200, sum thrust, count
20
+ })
21
+
22
+ test('transit floors distance then flight time', () => {
23
+ // distance = floor(sqrt(3^2+4^2)*10000)=50000; accel=400/mass*10000; flight=floor(2*sqrt(d/accel))
24
+ const mass = 1000
25
+ const accel = (400 / mass) * 10000
26
+ const expected = Math.floor(2 * Math.sqrt(50000 / accel))
27
+ expect(unwrapTransitDuration(mass, {x: 0, y: 0}, {x: 3, y: 4})).toBe(expected)
28
+ })
29
+
30
+ test('load uses altitude z, adds loader mass, divides by quantity', () => {
31
+ const loaders = {mass: 1200, thrust: 30, quantity: 2}
32
+ const itemMass = 800
33
+ const accel = (30 / (itemMass + 1200)) * 10000
34
+ const flight = Math.floor(2 * Math.sqrt(3000 / accel))
35
+ expect(unwrapLoadDuration(loaders, itemMass, 3000)).toBe(Math.floor(flight / 2))
36
+ })
37
+
38
+ test('zero item mass and no loaders are safe', () => {
39
+ expect(unwrapTransitDuration(0, {x: 0, y: 0}, {x: 9, y: 9})).toBe(0)
40
+ expect(unwrapLoadDuration(null, 500, 3000)).toBe(0)
41
+ })
42
+
43
+ test('load altitude is floored so ground-level (z=0) destinations are not instant', () => {
44
+ const loaders = {mass: 500, thrust: 200, quantity: 1}
45
+ const atZero = unwrapLoadDuration(loaders, 1000, 0)
46
+ const at800 = unwrapLoadDuration(loaders, 1000, 800)
47
+ expect(atZero).toBeGreaterThan(0)
48
+ expect(atZero).toBe(at800)
49
+ })
50
+
51
+ test('estimateUnwrapDuration uses worst-loader baseline when dest has no loaders', () => {
52
+ // IRON (101); same coords so transit is 0 and the estimate is baseline load alone
53
+ const item = {itemId: 101, quantity: 10, modules: [], originX: 0, originY: 0}
54
+ const bareDest = {loader_lanes: [], coordinates: {x: 0, y: 0, z: 800}}
55
+
56
+ const bare = estimateUnwrapDuration(bareDest, item)
57
+ expect(bare).toBeGreaterThan(0)
58
+
59
+ const loadedDest = {
60
+ loader_lanes: [{mass: 500, thrust: 200}],
61
+ coordinates: {x: 0, y: 0, z: 800},
62
+ }
63
+ const loaded = estimateUnwrapDuration(loadedDest, item)
64
+ expect(loaded).toBeLessThanOrEqual(bare)
65
+
66
+ expect(unwrapLoadDuration(null, 500, 3000)).toBe(0)
67
+ })
68
+ })
69
+
70
+ test('incomingHoldMass sums incoming-kind hold mass', () => {
71
+ expect(incomingHoldMass([])).toBe(0)
72
+ // PUSH(2) + FLIGHT(5) count; BUILD(4) does not
73
+ expect(
74
+ incomingHoldMass([
75
+ {kind: 2, incoming_mass: 100},
76
+ {kind: 4, incoming_mass: 999},
77
+ {kind: 5, incoming_mass: 50},
78
+ ])
79
+ ).toBe(150)
80
+ })
81
+
82
+ test('projectedPeakCargomass tracks the running peak from cargomass', () => {
83
+ const entity = {cargomass: 1000, lanes: [], cargo: [], schedule: undefined} as never
84
+ // No pending tasks: peak = base + candidate add.
85
+ expect(projectedPeakCargomass(entity, new Date(0), 500)).toBe(1500)
86
+ })