@shipload/sdk 1.0.0-next.7 → 1.0.0-next.71

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 (203) 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 +5822 -1484
  7. package/lib/shipload.js +20838 -5419
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +20674 -5669
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1691 -0
  12. package/lib/testing.js +7109 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +7103 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.test.ts +82 -0
  18. package/src/capabilities/craftable.ts +82 -0
  19. package/src/capabilities/crafting.test.ts +80 -0
  20. package/src/capabilities/crafting.ts +50 -8
  21. package/src/capabilities/gathering.test.ts +29 -0
  22. package/src/capabilities/gathering.ts +38 -20
  23. package/src/capabilities/hauling.ts +0 -5
  24. package/src/capabilities/index.ts +0 -1
  25. package/src/capabilities/modules.ts +27 -30
  26. package/src/capabilities/movement.ts +1 -1
  27. package/src/capabilities/storage.test.ts +26 -0
  28. package/src/capabilities/storage.ts +22 -7
  29. package/src/contracts/fund.ts +306 -0
  30. package/src/contracts/platform.ts +312 -3
  31. package/src/contracts/server.ts +2793 -478
  32. package/src/contracts/token.ts +170 -0
  33. package/src/contracts.ts +2 -0
  34. package/src/coordinates/address.ts +88 -0
  35. package/src/coordinates/constants.test.ts +15 -0
  36. package/src/coordinates/constants.ts +23 -0
  37. package/src/coordinates/index.ts +15 -0
  38. package/src/coordinates/memo.test.ts +47 -0
  39. package/src/coordinates/memo.ts +20 -0
  40. package/src/coordinates/permutation.ts +77 -0
  41. package/src/coordinates/regions.ts +48 -0
  42. package/src/coordinates/sectors.ts +115 -0
  43. package/src/data/capabilities.ts +49 -18
  44. package/src/data/capability-formulas.ts +26 -14
  45. package/src/data/catalog.test.ts +19 -0
  46. package/src/data/catalog.ts +13 -3
  47. package/src/data/charters.json +396 -0
  48. package/src/data/citizenry-adjectives.json +42 -0
  49. package/src/data/citizenry-nouns.json +50 -0
  50. package/src/data/colors.ts +14 -32
  51. package/src/data/entities.json +421 -15
  52. package/src/data/item-ids.ts +53 -13
  53. package/src/data/items.json +330 -77
  54. package/src/data/kind-registry.json +212 -0
  55. package/src/data/kind-registry.ts +192 -0
  56. package/src/data/metadata.ts +312 -51
  57. package/src/data/recipes-runtime.ts +6 -23
  58. package/src/data/recipes.json +2173 -173
  59. package/src/data/tiers.ts +9 -6
  60. package/src/derivation/build-methods.test.ts +71 -0
  61. package/src/derivation/build-methods.ts +50 -0
  62. package/src/derivation/capabilities.test.ts +348 -0
  63. package/src/derivation/capabilities.ts +707 -0
  64. package/src/derivation/capability-mappings.ts +60 -16
  65. package/src/derivation/crafting.test.ts +17 -0
  66. package/src/derivation/crafting.ts +57 -34
  67. package/src/derivation/index.ts +27 -2
  68. package/src/derivation/recipe-usage.test.ts +88 -0
  69. package/src/derivation/recipe-usage.ts +141 -0
  70. package/src/derivation/reserve-regen.ts +34 -0
  71. package/src/derivation/resources.ts +9 -1
  72. package/src/derivation/rollups.test.ts +55 -0
  73. package/src/derivation/rollups.ts +72 -0
  74. package/src/derivation/stars.test.ts +67 -0
  75. package/src/derivation/stars.ts +25 -0
  76. package/src/derivation/stat-scaling.ts +12 -0
  77. package/src/derivation/stats.ts +6 -6
  78. package/src/derivation/strata.ts +5 -3
  79. package/src/derivation/stratum.ts +26 -22
  80. package/src/derivation/tiers.ts +40 -7
  81. package/src/derivation/upgrades.ts +14 -0
  82. package/src/derivation/wormhole.ts +154 -0
  83. package/src/entities/entity.ts +102 -0
  84. package/src/entities/gamestate.ts +3 -28
  85. package/src/entities/makers.ts +144 -136
  86. package/src/entities/slot-multiplier.ts +43 -0
  87. package/src/entity/hydrate.test.ts +156 -0
  88. package/src/entity/hydrate.ts +86 -0
  89. package/src/errors.ts +21 -16
  90. package/src/format.ts +42 -9
  91. package/src/index-module.ts +425 -32
  92. package/src/influence/ballot.test.ts +103 -0
  93. package/src/influence/ballot.ts +98 -0
  94. package/src/influence/categories.ts +32 -0
  95. package/src/influence/charter-metadata.test.ts +136 -0
  96. package/src/influence/charter-metadata.ts +236 -0
  97. package/src/influence/charters.test.ts +112 -0
  98. package/src/influence/charters.ts +150 -0
  99. package/src/influence/citizenry.ts +38 -0
  100. package/src/influence/civic-shuttle.test.ts +202 -0
  101. package/src/influence/civic-shuttle.ts +142 -0
  102. package/src/influence/civic.test.ts +287 -0
  103. package/src/influence/civic.ts +269 -0
  104. package/src/influence/constants.ts +84 -0
  105. package/src/influence/decay.ts +20 -0
  106. package/src/influence/decomp.ts +95 -0
  107. package/src/influence/demand.ts +106 -0
  108. package/src/influence/duration.ts +101 -0
  109. package/src/influence/index.ts +118 -0
  110. package/src/influence/influence.test.ts +450 -0
  111. package/src/influence/quality.ts +25 -0
  112. package/src/influence/valuation.ts +154 -0
  113. package/src/managers/actions.ts +950 -93
  114. package/src/managers/balances.test.ts +161 -0
  115. package/src/managers/balances.ts +93 -0
  116. package/src/managers/base.ts +10 -2
  117. package/src/managers/cluster.test.ts +39 -0
  118. package/src/managers/cluster.ts +53 -0
  119. package/src/managers/construction-types.ts +80 -0
  120. package/src/managers/construction.ts +453 -0
  121. package/src/managers/context.ts +58 -1
  122. package/src/managers/coordinates.ts +14 -0
  123. package/src/managers/entities.ts +35 -63
  124. package/src/managers/epochs.ts +72 -1
  125. package/src/managers/index.ts +38 -1
  126. package/src/managers/influence.test.ts +56 -0
  127. package/src/managers/influence.ts +425 -0
  128. package/src/managers/jobs.test.ts +96 -0
  129. package/src/managers/jobs.ts +58 -0
  130. package/src/managers/locations.ts +25 -29
  131. package/src/managers/nft.test.ts +21 -0
  132. package/src/managers/nft.ts +110 -0
  133. package/src/managers/players.ts +25 -0
  134. package/src/managers/plot.ts +122 -0
  135. package/src/nft/atomicassets.abi.json +1342 -0
  136. package/src/nft/atomicassets.ts +237 -0
  137. package/src/nft/atomicdata.ts +130 -0
  138. package/src/nft/buildImmutableData.ts +388 -0
  139. package/src/nft/description.ts +222 -100
  140. package/src/nft/index.ts +3 -0
  141. package/src/planner/index.ts +296 -0
  142. package/src/planner/planner.test.ts +459 -0
  143. package/src/receipts.ts +22 -0
  144. package/src/resolution/describe-module.ts +43 -25
  145. package/src/resolution/display-name.ts +39 -11
  146. package/src/resolution/resolve-item.test.ts +48 -0
  147. package/src/resolution/resolve-item.ts +112 -58
  148. package/src/scan/index.ts +244 -0
  149. package/src/scan/scan-wasm.base64.ts +2 -0
  150. package/src/scheduling/accessor.ts +65 -23
  151. package/src/scheduling/availability.test.ts +320 -0
  152. package/src/scheduling/availability.ts +251 -0
  153. package/src/scheduling/cancel.test.ts +550 -0
  154. package/src/scheduling/cancel.ts +254 -0
  155. package/src/scheduling/cluster-stock.test.ts +31 -0
  156. package/src/scheduling/cluster-stock.ts +15 -0
  157. package/src/scheduling/energy.ts +47 -0
  158. package/src/scheduling/epoch.ts +1 -0
  159. package/src/scheduling/idle-resolve.ts +45 -0
  160. package/src/scheduling/jobs.test.ts +448 -0
  161. package/src/scheduling/jobs.ts +343 -0
  162. package/src/scheduling/lane-core.ts +128 -0
  163. package/src/scheduling/lanes.test.ts +250 -0
  164. package/src/scheduling/lanes.ts +279 -0
  165. package/src/scheduling/projection.ts +265 -149
  166. package/src/scheduling/schedule.test.ts +163 -0
  167. package/src/scheduling/schedule.ts +282 -116
  168. package/src/scheduling/task-cargo.test.ts +110 -0
  169. package/src/scheduling/task-cargo.ts +62 -0
  170. package/src/scheduling/task-effects.test.ts +35 -0
  171. package/src/scheduling/task-effects.ts +34 -0
  172. package/src/scheduling/unwrap.test.ts +86 -0
  173. package/src/scheduling/unwrap.ts +190 -0
  174. package/src/shipload.ts +63 -4
  175. package/src/subscriptions/manager.cluster.test.ts +51 -0
  176. package/src/subscriptions/manager.ts +245 -172
  177. package/src/subscriptions/mappers.ts +5 -8
  178. package/src/subscriptions/types.ts +28 -3
  179. package/src/testing/catalog-hash.ts +19 -0
  180. package/src/testing/index.ts +2 -0
  181. package/src/testing/projection-parity.ts +167 -0
  182. package/src/travel/reach.test.ts +86 -0
  183. package/src/travel/reach.ts +34 -0
  184. package/src/travel/route-build.test.ts +63 -0
  185. package/src/travel/route-build.ts +60 -0
  186. package/src/travel/route-planner.ts +288 -0
  187. package/src/travel/route-simulator.ts +170 -0
  188. package/src/travel/travel.ts +190 -123
  189. package/src/types/capabilities.ts +37 -8
  190. package/src/types/entity.ts +3 -3
  191. package/src/types/index.ts +0 -1
  192. package/src/types.ts +81 -23
  193. package/src/utils/cargo.test.ts +14 -0
  194. package/src/utils/cargo.ts +2 -0
  195. package/src/utils/display-name.ts +70 -0
  196. package/src/utils/hash.ts +22 -2
  197. package/src/utils/system.ts +24 -5
  198. package/src/capabilities/loading.ts +0 -8
  199. package/src/entities/container.ts +0 -123
  200. package/src/entities/ship-deploy.ts +0 -295
  201. package/src/entities/ship.ts +0 -221
  202. package/src/entities/warehouse.ts +0 -127
  203. package/src/types/entity-traits.ts +0 -69
@@ -0,0 +1,343 @@
1
+ import type {ServerContract} from '../contracts'
2
+ import {TaskType} from '../types'
3
+ import type {OrderedTask} from './schedule'
4
+
5
+ type CargoItem = ServerContract.Types.cargo_item
6
+
7
+ export interface JobWindow {
8
+ id: number
9
+ socket: number
10
+ owner: string
11
+ startsAt: Date
12
+ completesAt: Date
13
+ /** Contract-recorded completion of the input Drop-off. */
14
+ arrivesAt?: Date
15
+ recipeId: number
16
+ quantity: number
17
+ /** False while the job's inputs are still in transit to the building. */
18
+ deposited: boolean
19
+ /** Packed stat roll of the job's output, when the source carried the job's cargo. */
20
+ outputStats?: bigint
21
+ /** Ship that booked the job; lets a caller find its Drop-off on that ship's schedule. */
22
+ shipId?: number
23
+ /** Inputs the Drop-off carries; matched against the task cargo to find the right Drop-off. */
24
+ inputs?: CargoItem[]
25
+ }
26
+
27
+ export interface JobLaneEntry {
28
+ kind: 'idle' | 'job'
29
+ startsAt: Date
30
+ completesAt: Date
31
+ job?: JobWindow
32
+ }
33
+
34
+ export interface JobLane {
35
+ socket: number
36
+ entries: JobLaneEntry[]
37
+ }
38
+
39
+ export const JOB_QUEUE_CAP = 25
40
+
41
+ function activeOn(jobs: JobWindow[], socket: number, now: Date): JobWindow[] {
42
+ return jobs
43
+ .filter((j) => j.socket === socket && j.completesAt > now)
44
+ .sort((a, b) => a.startsAt.getTime() - b.startsAt.getTime())
45
+ }
46
+
47
+ export function jobsToLanes(jobs: JobWindow[], socketCount: number, now: Date): JobLane[] {
48
+ const lanes: JobLane[] = []
49
+ for (let socket = 0; socket < socketCount; socket++) {
50
+ const entries: JobLaneEntry[] = []
51
+ let cursor: Date | null = null
52
+ for (const j of activeOn(jobs, socket, now)) {
53
+ if (cursor && j.startsAt > cursor) {
54
+ entries.push({kind: 'idle', startsAt: cursor, completesAt: j.startsAt})
55
+ }
56
+ entries.push({kind: 'job', startsAt: j.startsAt, completesAt: j.completesAt, job: j})
57
+ cursor = j.completesAt
58
+ }
59
+ lanes.push({socket, entries})
60
+ }
61
+ return lanes
62
+ }
63
+
64
+ export function socketTail(jobs: JobWindow[], socket: number, now: Date): Date {
65
+ const active = activeOn(jobs, socket, now)
66
+ const last = active[active.length - 1]
67
+ return last && last.completesAt > now ? last.completesAt : now
68
+ }
69
+
70
+ // Display-only estimate of the window a booking would receive; the contract picks the socket.
71
+ export function pickFabricator(
72
+ jobs: JobWindow[],
73
+ sockets: Array<{open: boolean}>,
74
+ durationBySocketMinutes: number[],
75
+ arrival: Date,
76
+ now: Date = arrival
77
+ ): {slot: number; startsAt: Date; completesAt: Date} | null {
78
+ let best: {slot: number; startsAt: Date; completesAt: Date} | null = null
79
+ for (let slot = 0; slot < sockets.length; slot++) {
80
+ if (!sockets[slot].open) continue
81
+ if (activeOn(jobs, slot, now).length >= JOB_QUEUE_CAP) continue
82
+ const durationMs = durationBySocketMinutes[slot] * 60_000
83
+ let startsAt = arrival
84
+ for (const job of activeOn(jobs, slot, now)) {
85
+ if (job.completesAt <= startsAt) continue
86
+ if (job.startsAt.getTime() - startsAt.getTime() >= durationMs) break
87
+ startsAt = job.completesAt
88
+ }
89
+ const completesAt = new Date(startsAt.getTime() + durationMs)
90
+ if (!best || completesAt < best.completesAt) {
91
+ best = {slot, startsAt, completesAt}
92
+ }
93
+ }
94
+ return best
95
+ }
96
+
97
+ export function workshopAvailability(
98
+ jobs: JobWindow[],
99
+ socketCount: number,
100
+ now: Date
101
+ ): {open: number; nextOpeningAt: Date | null} {
102
+ let open = 0
103
+ let nextOpeningAt: Date | null = null
104
+ for (let socket = 0; socket < socketCount; socket++) {
105
+ const tail = socketTail(jobs, socket, now)
106
+ if (tail <= now) {
107
+ open++
108
+ } else if (!nextOpeningAt || tail < nextOpeningAt) {
109
+ nextOpeningAt = tail
110
+ }
111
+ }
112
+ return {open, nextOpeningAt: open > 0 ? null : nextOpeningAt}
113
+ }
114
+
115
+ export type JobStatus = 'booked' | 'dropping' | 'queued' | 'crafting' | 'ready' | 'pickingup'
116
+
117
+ export interface JobStatusInput {
118
+ startsAt: Date
119
+ completesAt: Date
120
+ /** Contract-recorded completion of the input Drop-off. */
121
+ arrivesAt?: Date
122
+ deposited?: boolean
123
+ quantity?: number
124
+ building?: number
125
+ inputs?: readonly CargoItem[]
126
+ }
127
+
128
+ function modulesEqual(a: CargoItem['modules'], b: CargoItem['modules']): boolean {
129
+ const length = Math.max(a?.length ?? 0, b?.length ?? 0)
130
+ for (let i = 0; i < length; i++) {
131
+ const installed = a?.[i]?.installed
132
+ const other = b?.[i]?.installed
133
+ if (Boolean(installed) !== Boolean(other)) return false
134
+ if (
135
+ installed &&
136
+ other &&
137
+ (String(installed.item_id) !== String(other.item_id) ||
138
+ String(installed.stats) !== String(other.stats))
139
+ ) {
140
+ return false
141
+ }
142
+ }
143
+ return true
144
+ }
145
+
146
+ function cargoEquals(a: readonly CargoItem[], b: readonly CargoItem[]): boolean {
147
+ if (a.length !== b.length) return false
148
+ return a.every((x, i) => {
149
+ const y = b[i]
150
+ if (
151
+ String(x.item_id) !== String(y.item_id) ||
152
+ String(x.stats) !== String(y.stats) ||
153
+ String(x.quantity) !== String(y.quantity) ||
154
+ String(x.entity_id ?? 0) !== String(y.entity_id ?? 0) ||
155
+ !modulesEqual(x.modules, y.modules)
156
+ ) {
157
+ return false
158
+ }
159
+ return true
160
+ })
161
+ }
162
+
163
+ function matchesJob(t: OrderedTask, job: JobStatusInput): boolean {
164
+ const subject = t.task.subject
165
+ if (job.building !== undefined && Number(subject?.entity_id) !== job.building) return false
166
+ if (job.inputs !== undefined && !cargoEquals(t.task.cargo, job.inputs)) return false
167
+ return true
168
+ }
169
+
170
+ export function jobDropoffTask(
171
+ job: JobStatusInput,
172
+ tasks: readonly OrderedTask[] | undefined
173
+ ): OrderedTask | undefined {
174
+ const matches = tasks?.filter(
175
+ (t) =>
176
+ Number(t.task.type) === TaskType.CIVIC_DEPOSIT &&
177
+ matchesJob(t, job) &&
178
+ (job.arrivesAt === undefined || t.completesAt.getTime() === job.arrivesAt.getTime())
179
+ )
180
+ return matches?.length === 1 ? matches[0] : undefined
181
+ }
182
+
183
+ // Without the ship's schedule an undeposited row reads as Dropping off: the transfer usually starts at once.
184
+ export function jobStatus(
185
+ job: JobStatusInput,
186
+ now: Date,
187
+ tasks?: readonly OrderedTask[]
188
+ ): JobStatus {
189
+ if (job.deposited === false) {
190
+ const dropoff = jobDropoffTask(job, tasks)
191
+ return dropoff && now < dropoff.startsAt ? 'booked' : 'dropping'
192
+ }
193
+ if (job.quantity === 0) return 'ready'
194
+ if (now < job.startsAt) return 'queued'
195
+ if (now < job.completesAt) return 'crafting'
196
+ return 'ready'
197
+ }
198
+
199
+ export function jobCancellable(status: JobStatus): boolean {
200
+ return status === 'booked' || status === 'dropping' || status === 'queued'
201
+ }
202
+
203
+ export type JobCancellationBlockReason = 'legacy-inputs-unavailable' | 'ambiguous-dropoff'
204
+
205
+ /** A migrated output-only job has no retained inputs that modern cancellation could return. */
206
+ export function jobCancellationBlockReason(
207
+ job: JobStatusInput,
208
+ tasks?: readonly OrderedTask[]
209
+ ): JobCancellationBlockReason | null {
210
+ if (
211
+ job.deposited === true &&
212
+ (job.quantity ?? 0) > 0 &&
213
+ job.inputs !== undefined &&
214
+ job.inputs.length === 0
215
+ ) {
216
+ return 'legacy-inputs-unavailable'
217
+ }
218
+ if (job.deposited === false && job.arrivesAt !== undefined && tasks) {
219
+ const matches = tasks.filter(
220
+ (task) =>
221
+ Number(task.task.type) === TaskType.CIVIC_DEPOSIT &&
222
+ matchesJob(task, job) &&
223
+ task.completesAt.getTime() === job.arrivesAt?.getTime()
224
+ )
225
+ if (matches.length > 1) return 'ambiguous-dropoff'
226
+ }
227
+ return null
228
+ }
229
+
230
+ export type JobCancelRoute =
231
+ | {kind: 'dropoff'; shipId: number; laneKey: number; count: number}
232
+ | {kind: 'craft'; jobId: number}
233
+ | {kind: 'build'; jobId: number}
234
+
235
+ // The ship's cancel pops from the lane tail, so the count reaches from the Drop-off through every task queued behind it.
236
+ function cancelRoute(
237
+ job: JobStatusInput & {id: number; shipId?: number},
238
+ queued: 'craft' | 'build',
239
+ now: Date,
240
+ tasks?: readonly OrderedTask[]
241
+ ): JobCancelRoute | null {
242
+ if (!jobCancellable(jobStatus(job, now, tasks))) return null
243
+ if (queued === 'craft' && jobCancellationBlockReason(job, tasks)) return null
244
+ if (job.deposited !== false) return {kind: queued, jobId: job.id}
245
+ if (job.shipId === undefined || !tasks) return null
246
+ const dropoff = jobDropoffTask(job, tasks)
247
+ if (!dropoff || dropoff.completesAt <= now) return null
248
+ const laneLength = tasks.filter((t) => t.laneKey === dropoff.laneKey).length
249
+ return {
250
+ kind: 'dropoff',
251
+ shipId: job.shipId,
252
+ laneKey: dropoff.laneKey,
253
+ count: laneLength - dropoff.taskIndex,
254
+ }
255
+ }
256
+
257
+ export function jobCancelRoute(
258
+ job: JobStatusInput & {id: number; shipId?: number},
259
+ now: Date,
260
+ tasks?: readonly OrderedTask[]
261
+ ): JobCancelRoute | null {
262
+ return cancelRoute(job, 'craft', now, tasks)
263
+ }
264
+
265
+ // A Build Job's Drop-off rides on the upgrade target itself, so the target plays the ship's part.
266
+ export function buildJobCancelRoute(
267
+ job: JobStatusInput & {id: number; targetId?: number},
268
+ now: Date,
269
+ tasks?: readonly OrderedTask[]
270
+ ): JobCancelRoute | null {
271
+ return cancelRoute({...job, shipId: job.targetId}, 'build', now, tasks)
272
+ }
273
+
274
+ export interface PickupInFlight {
275
+ building: number
276
+ cargo: CargoItem[]
277
+ startsAt: Date
278
+ completesAt: Date
279
+ }
280
+
281
+ export function pickupsInFlight(tasks: OrderedTask[], building?: number): PickupInFlight[] {
282
+ return tasks
283
+ .filter(
284
+ (t) =>
285
+ Number(t.task.type) === TaskType.CIVIC_WITHDRAW &&
286
+ (building === undefined || Number(t.task.subject?.entity_id) === building)
287
+ )
288
+ .map((t) => ({
289
+ building: Number(t.task.subject?.entity_id),
290
+ cargo: [...t.task.cargo],
291
+ startsAt: t.startsAt,
292
+ completesAt: t.completesAt,
293
+ }))
294
+ }
295
+
296
+ const JOB_STATUS_LABELS: Record<JobStatus, string> = {
297
+ booked: 'Booked',
298
+ dropping: 'Dropping off',
299
+ queued: 'Queued',
300
+ crafting: 'Crafting',
301
+ ready: 'Ready for Pickup',
302
+ pickingup: 'Picking up',
303
+ }
304
+
305
+ export function jobStatusLabel(status: JobStatus): string {
306
+ return JOB_STATUS_LABELS[status]
307
+ }
308
+
309
+ // A row written before the deposited flag existed reads as undefined; those jobs were all deposited.
310
+ export function jobDeposited(value: unknown): boolean {
311
+ if (value === undefined || value === null) return true
312
+ return Boolean(value)
313
+ }
314
+
315
+ // A deposited row's output is the cargo tail; a cancelled row (quantity 0) holds only its inputs.
316
+ export function splitJobCargo<T>(
317
+ cargo: readonly T[],
318
+ deposited: boolean,
319
+ quantity: number
320
+ ): {output: T | null; inputs: T[]} {
321
+ if (cargo.length === 0) return {output: null, inputs: []}
322
+ if (!deposited || quantity === 0) return {output: null, inputs: [...cargo]}
323
+ return {output: cargo[cargo.length - 1], inputs: cargo.slice(0, -1)}
324
+ }
325
+
326
+ export interface OwnedJob {
327
+ id: number
328
+ building: number
329
+ socket: number
330
+ shipId: number
331
+ coords: {x: number; y: number}
332
+ startsAt: Date
333
+ completesAt: Date
334
+ arrivesAt: Date
335
+ recipeId: number
336
+ quantity: number
337
+ status: JobStatus
338
+ deposited: boolean
339
+ output: CargoItem | null
340
+ inputs: CargoItem[]
341
+ /** Packed stat roll of `output`, so every job shape answers this the same way. */
342
+ outputStats?: bigint
343
+ }
@@ -0,0 +1,128 @@
1
+ import type {ServerContract} from '../contracts'
2
+ import type {TaskType} from '../types'
3
+
4
+ type Schedule = ServerContract.Types.schedule
5
+ type Task = ServerContract.Types.task
6
+
7
+ export function laneDuration(schedule: Schedule): number {
8
+ return schedule.tasks.reduce((sum, task) => sum + task.duration.toNumber(), 0)
9
+ }
10
+
11
+ export function laneRawElapsed(schedule: Schedule, now: Date): number {
12
+ const started = schedule.started.toDate()
13
+ return Math.floor((now.getTime() - started.getTime()) / 1000)
14
+ }
15
+
16
+ export function laneElapsed(schedule: Schedule, now: Date): number {
17
+ return Math.max(0, laneRawElapsed(schedule, now))
18
+ }
19
+
20
+ export function laneStartsIn(schedule: Schedule, now: Date): number {
21
+ return Math.max(0, -laneRawElapsed(schedule, now))
22
+ }
23
+
24
+ export function laneRemaining(schedule: Schedule, now: Date): number {
25
+ return Math.max(0, laneDuration(schedule) - laneRawElapsed(schedule, now))
26
+ }
27
+
28
+ export function laneComplete(schedule: Schedule, now: Date): boolean {
29
+ if (schedule.tasks.length === 0) return false
30
+ return laneRemaining(schedule, now) === 0
31
+ }
32
+
33
+ export function laneProgress(schedule: Schedule, now: Date): number {
34
+ const duration = laneDuration(schedule)
35
+ if (duration === 0) return schedule.tasks.length > 0 ? 1 : 0
36
+ return Math.min(1, laneElapsed(schedule, now) / duration)
37
+ }
38
+
39
+ export function currentTaskIndexForLane(schedule: Schedule, now: Date): number {
40
+ if (schedule.tasks.length === 0) return -1
41
+ if (laneRawElapsed(schedule, now) < 0) return -1
42
+ const elapsed = laneElapsed(schedule, now)
43
+ let timeAccum = 0
44
+ for (let i = 0; i < schedule.tasks.length; i++) {
45
+ const taskDuration = schedule.tasks[i].duration.toNumber()
46
+ if (elapsed < timeAccum + taskDuration) return i
47
+ timeAccum += taskDuration
48
+ }
49
+ return -1
50
+ }
51
+
52
+ export function currentTask(schedule: Schedule, now: Date): Task | undefined {
53
+ const index = currentTaskIndexForLane(schedule, now)
54
+ if (index < 0) return undefined
55
+ return schedule.tasks[index]
56
+ }
57
+
58
+ export function currentTaskType(schedule: Schedule, now: Date): TaskType | undefined {
59
+ const task = currentTask(schedule, now)
60
+ return task ? (task.type.toNumber() as TaskType) : undefined
61
+ }
62
+
63
+ export function laneTaskStartTime(schedule: Schedule, index: number): number {
64
+ if (index < 0 || index >= schedule.tasks.length) return 0
65
+ let timeAccum = 0
66
+ for (let i = 0; i < index; i++) {
67
+ timeAccum += schedule.tasks[i].duration.toNumber()
68
+ }
69
+ return timeAccum
70
+ }
71
+
72
+ export function laneTaskElapsed(schedule: Schedule, index: number, now: Date): number {
73
+ if (index < 0 || index >= schedule.tasks.length) return 0
74
+ const elapsed = laneElapsed(schedule, now)
75
+ const taskStart = laneTaskStartTime(schedule, index)
76
+ const taskDuration = schedule.tasks[index].duration.toNumber()
77
+ if (elapsed <= taskStart) return 0
78
+ return Math.min(elapsed - taskStart, taskDuration)
79
+ }
80
+
81
+ export function laneTaskRemaining(schedule: Schedule, index: number, now: Date): number {
82
+ if (index < 0 || index >= schedule.tasks.length) return 0
83
+ const taskDuration = schedule.tasks[index].duration.toNumber()
84
+ return Math.max(0, taskDuration - laneTaskElapsed(schedule, index, now))
85
+ }
86
+
87
+ export function laneTaskComplete(schedule: Schedule, index: number, now: Date): boolean {
88
+ if (index < 0 || index >= schedule.tasks.length) return false
89
+ const taskDuration = schedule.tasks[index].duration.toNumber()
90
+ return laneTaskElapsed(schedule, index, now) >= taskDuration
91
+ }
92
+
93
+ export function laneTaskInProgress(schedule: Schedule, index: number, now: Date): boolean {
94
+ if (index < 0 || index >= schedule.tasks.length) return false
95
+ const taskElapsed = laneTaskElapsed(schedule, index, now)
96
+ const taskDuration = schedule.tasks[index].duration.toNumber()
97
+ return taskElapsed > 0 && taskElapsed < taskDuration
98
+ }
99
+
100
+ export function laneCompletesAt(schedule: Schedule, index: number): Date {
101
+ const startedMs = schedule.started.toDate().getTime()
102
+ const endSec =
103
+ laneTaskStartTime(schedule, index) + (schedule.tasks[index]?.duration.toNumber() ?? 0)
104
+ return new Date(startedMs + endSec * 1000)
105
+ }
106
+
107
+ export function currentTaskProgress(schedule: Schedule, now: Date): number {
108
+ const index = currentTaskIndexForLane(schedule, now)
109
+ if (index < 0) return 0
110
+ const elapsed = laneTaskElapsed(schedule, index, now)
111
+ const duration = schedule.tasks[index].duration.toNumber()
112
+ if (duration === 0) return 1
113
+ return Math.min(1, elapsed / duration)
114
+ }
115
+
116
+ export function currentTaskProgressFloatForLane(schedule: Schedule, now: Date): number {
117
+ if (schedule.tasks.length === 0) return 0
118
+ const index = currentTaskIndexForLane(schedule, now)
119
+ if (index < 0) return 0
120
+ const task = schedule.tasks[index]
121
+ const durationMs = task.duration.toNumber() * 1000
122
+ if (durationMs === 0) return 1
123
+ const startedMs = schedule.started.toDate().getTime()
124
+ const taskStartMs = startedMs + laneTaskStartTime(schedule, index) * 1000
125
+ const elapsedMs = now.getTime() - taskStartMs
126
+ if (elapsedMs <= 0) return 0
127
+ return Math.min(1, elapsedMs / durationMs)
128
+ }