@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.
- package/lib/scan.d.ts +52 -0
- package/lib/scan.js +162 -0
- package/lib/scan.js.map +1 -0
- package/lib/scan.m.js +152 -0
- package/lib/scan.m.js.map +1 -0
- package/lib/shipload.d.ts +5822 -1484
- package/lib/shipload.js +20838 -5419
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +20674 -5669
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +1691 -0
- package/lib/testing.js +7109 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +7103 -0
- package/lib/testing.m.js.map +1 -0
- package/package.json +20 -2
- package/src/capabilities/craftable.test.ts +82 -0
- package/src/capabilities/craftable.ts +82 -0
- package/src/capabilities/crafting.test.ts +80 -0
- package/src/capabilities/crafting.ts +50 -8
- package/src/capabilities/gathering.test.ts +29 -0
- package/src/capabilities/gathering.ts +38 -20
- package/src/capabilities/hauling.ts +0 -5
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +27 -30
- package/src/capabilities/movement.ts +1 -1
- package/src/capabilities/storage.test.ts +26 -0
- package/src/capabilities/storage.ts +22 -7
- package/src/contracts/fund.ts +306 -0
- package/src/contracts/platform.ts +312 -3
- package/src/contracts/server.ts +2793 -478
- package/src/contracts/token.ts +170 -0
- package/src/contracts.ts +2 -0
- package/src/coordinates/address.ts +88 -0
- package/src/coordinates/constants.test.ts +15 -0
- package/src/coordinates/constants.ts +23 -0
- package/src/coordinates/index.ts +15 -0
- package/src/coordinates/memo.test.ts +47 -0
- package/src/coordinates/memo.ts +20 -0
- package/src/coordinates/permutation.ts +77 -0
- package/src/coordinates/regions.ts +48 -0
- package/src/coordinates/sectors.ts +115 -0
- package/src/data/capabilities.ts +49 -18
- package/src/data/capability-formulas.ts +26 -14
- package/src/data/catalog.test.ts +19 -0
- package/src/data/catalog.ts +13 -3
- package/src/data/charters.json +396 -0
- package/src/data/citizenry-adjectives.json +42 -0
- package/src/data/citizenry-nouns.json +50 -0
- package/src/data/colors.ts +14 -32
- package/src/data/entities.json +421 -15
- package/src/data/item-ids.ts +53 -13
- package/src/data/items.json +330 -77
- package/src/data/kind-registry.json +212 -0
- package/src/data/kind-registry.ts +192 -0
- package/src/data/metadata.ts +312 -51
- package/src/data/recipes-runtime.ts +6 -23
- package/src/data/recipes.json +2173 -173
- package/src/data/tiers.ts +9 -6
- package/src/derivation/build-methods.test.ts +71 -0
- package/src/derivation/build-methods.ts +50 -0
- package/src/derivation/capabilities.test.ts +348 -0
- package/src/derivation/capabilities.ts +707 -0
- package/src/derivation/capability-mappings.ts +60 -16
- package/src/derivation/crafting.test.ts +17 -0
- package/src/derivation/crafting.ts +57 -34
- package/src/derivation/index.ts +27 -2
- package/src/derivation/recipe-usage.test.ts +88 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +9 -1
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +72 -0
- package/src/derivation/stars.test.ts +67 -0
- package/src/derivation/stars.ts +25 -0
- package/src/derivation/stat-scaling.ts +12 -0
- package/src/derivation/stats.ts +6 -6
- package/src/derivation/strata.ts +5 -3
- package/src/derivation/stratum.ts +26 -22
- package/src/derivation/tiers.ts +40 -7
- package/src/derivation/upgrades.ts +14 -0
- package/src/derivation/wormhole.ts +154 -0
- package/src/entities/entity.ts +102 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +144 -136
- package/src/entities/slot-multiplier.ts +43 -0
- package/src/entity/hydrate.test.ts +156 -0
- package/src/entity/hydrate.ts +86 -0
- package/src/errors.ts +21 -16
- package/src/format.ts +42 -9
- package/src/index-module.ts +425 -32
- package/src/influence/ballot.test.ts +103 -0
- package/src/influence/ballot.ts +98 -0
- package/src/influence/categories.ts +32 -0
- package/src/influence/charter-metadata.test.ts +136 -0
- package/src/influence/charter-metadata.ts +236 -0
- package/src/influence/charters.test.ts +112 -0
- package/src/influence/charters.ts +150 -0
- package/src/influence/citizenry.ts +38 -0
- package/src/influence/civic-shuttle.test.ts +202 -0
- package/src/influence/civic-shuttle.ts +142 -0
- package/src/influence/civic.test.ts +287 -0
- package/src/influence/civic.ts +269 -0
- package/src/influence/constants.ts +84 -0
- package/src/influence/decay.ts +20 -0
- package/src/influence/decomp.ts +95 -0
- package/src/influence/demand.ts +106 -0
- package/src/influence/duration.ts +101 -0
- package/src/influence/index.ts +118 -0
- package/src/influence/influence.test.ts +450 -0
- package/src/influence/quality.ts +25 -0
- package/src/influence/valuation.ts +154 -0
- package/src/managers/actions.ts +950 -93
- package/src/managers/balances.test.ts +161 -0
- package/src/managers/balances.ts +93 -0
- package/src/managers/base.ts +10 -2
- package/src/managers/cluster.test.ts +39 -0
- package/src/managers/cluster.ts +53 -0
- package/src/managers/construction-types.ts +80 -0
- package/src/managers/construction.ts +453 -0
- package/src/managers/context.ts +58 -1
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/entities.ts +35 -63
- package/src/managers/epochs.ts +72 -1
- package/src/managers/index.ts +38 -1
- package/src/managers/influence.test.ts +56 -0
- package/src/managers/influence.ts +425 -0
- package/src/managers/jobs.test.ts +96 -0
- package/src/managers/jobs.ts +58 -0
- package/src/managers/locations.ts +25 -29
- package/src/managers/nft.test.ts +21 -0
- package/src/managers/nft.ts +110 -0
- package/src/managers/players.ts +25 -0
- package/src/managers/plot.ts +122 -0
- package/src/nft/atomicassets.abi.json +1342 -0
- package/src/nft/atomicassets.ts +237 -0
- package/src/nft/atomicdata.ts +130 -0
- package/src/nft/buildImmutableData.ts +388 -0
- package/src/nft/description.ts +222 -100
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +296 -0
- package/src/planner/planner.test.ts +459 -0
- package/src/receipts.ts +22 -0
- package/src/resolution/describe-module.ts +43 -25
- package/src/resolution/display-name.ts +39 -11
- package/src/resolution/resolve-item.test.ts +48 -0
- package/src/resolution/resolve-item.ts +112 -58
- package/src/scan/index.ts +244 -0
- package/src/scan/scan-wasm.base64.ts +2 -0
- package/src/scheduling/accessor.ts +65 -23
- package/src/scheduling/availability.test.ts +320 -0
- package/src/scheduling/availability.ts +251 -0
- package/src/scheduling/cancel.test.ts +550 -0
- package/src/scheduling/cancel.ts +254 -0
- package/src/scheduling/cluster-stock.test.ts +31 -0
- package/src/scheduling/cluster-stock.ts +15 -0
- package/src/scheduling/energy.ts +47 -0
- package/src/scheduling/epoch.ts +1 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/jobs.test.ts +448 -0
- package/src/scheduling/jobs.ts +343 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +250 -0
- package/src/scheduling/lanes.ts +279 -0
- package/src/scheduling/projection.ts +265 -149
- package/src/scheduling/schedule.test.ts +163 -0
- package/src/scheduling/schedule.ts +282 -116
- package/src/scheduling/task-cargo.test.ts +110 -0
- package/src/scheduling/task-cargo.ts +62 -0
- package/src/scheduling/task-effects.test.ts +35 -0
- package/src/scheduling/task-effects.ts +34 -0
- package/src/scheduling/unwrap.test.ts +86 -0
- package/src/scheduling/unwrap.ts +190 -0
- package/src/shipload.ts +63 -4
- package/src/subscriptions/manager.cluster.test.ts +51 -0
- package/src/subscriptions/manager.ts +245 -172
- package/src/subscriptions/mappers.ts +5 -8
- package/src/subscriptions/types.ts +28 -3
- package/src/testing/catalog-hash.ts +19 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/projection-parity.ts +167 -0
- package/src/travel/reach.test.ts +86 -0
- package/src/travel/reach.ts +34 -0
- package/src/travel/route-build.test.ts +63 -0
- package/src/travel/route-build.ts +60 -0
- package/src/travel/route-planner.ts +288 -0
- package/src/travel/route-simulator.ts +170 -0
- package/src/travel/travel.ts +190 -123
- package/src/types/capabilities.ts +37 -8
- package/src/types/entity.ts +3 -3
- package/src/types/index.ts +0 -1
- package/src/types.ts +81 -23
- package/src/utils/cargo.test.ts +14 -0
- package/src/utils/cargo.ts +2 -0
- package/src/utils/display-name.ts +70 -0
- package/src/utils/hash.ts +22 -2
- package/src/utils/system.ts +24 -5
- package/src/capabilities/loading.ts +0 -8
- package/src/entities/container.ts +0 -123
- package/src/entities/ship-deploy.ts +0 -295
- package/src/entities/ship.ts +0 -221
- package/src/entities/warehouse.ts +0 -127
- 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
|
+
}
|