@shipload/sdk 1.0.0-next.26 → 1.0.0-next.27

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.
@@ -0,0 +1,60 @@
1
+ import type {ServerContract} from '../contracts'
2
+ import {getItem} from '../data/catalog'
3
+ import type {ModuleType} from '../types'
4
+ import {getLane, type ScheduleData} from './schedule'
5
+
6
+ type ModuleEntry = ServerContract.Types.module_entry
7
+ type Lane = ServerContract.Types.lane
8
+ type Schedule = ServerContract.Types.schedule
9
+
10
+ export function laneKeyForModule(slotIndex: number): number {
11
+ return slotIndex + 1
12
+ }
13
+
14
+ function laneIsFree(lanes: Lane[], laneKey: number): boolean {
15
+ const lane = lanes.find((entry) => entry.lane_key.toNumber() === laneKey)
16
+ return lane ? lane.schedule.tasks.length === 0 : true
17
+ }
18
+
19
+ export function workerLaneKey(
20
+ modules: ModuleEntry[],
21
+ moduleSubtype: ModuleType,
22
+ lanes: Lane[]
23
+ ): number {
24
+ const occupiedMatchingLaneKeys: number[] = []
25
+
26
+ for (let slotIndex = 0; slotIndex < modules.length; slotIndex++) {
27
+ const installed = modules[slotIndex].installed
28
+ if (!installed) continue
29
+ if (getItem(installed.item_id).moduleType !== moduleSubtype) continue
30
+
31
+ const laneKey = laneKeyForModule(slotIndex)
32
+ if (laneIsFree(lanes, laneKey)) return laneKey
33
+ occupiedMatchingLaneKeys.push(laneKey)
34
+ }
35
+
36
+ if (occupiedMatchingLaneKeys.length > 0) {
37
+ return Math.min(...occupiedMatchingLaneKeys)
38
+ }
39
+
40
+ throw new Error(`No installed ${moduleSubtype} worker module`)
41
+ }
42
+
43
+ export function rawScheduleEnd(schedule: Schedule): Date {
44
+ const durationSec = schedule.tasks.reduce((sum, task) => sum + task.duration.toNumber(), 0)
45
+ return new Date(schedule.started.toDate().getTime() + durationSec * 1000)
46
+ }
47
+
48
+ export function candidateLaneCompletesAt(
49
+ entity: ScheduleData,
50
+ laneKey: number,
51
+ durationSec: number,
52
+ now: Date
53
+ ): Date {
54
+ const lane = getLane(entity, laneKey)
55
+ const startMs = lane
56
+ ? Math.max(rawScheduleEnd(lane.schedule).getTime(), now.getTime())
57
+ : now.getTime()
58
+
59
+ return new Date(startMs + durationSec * 1000)
60
+ }
@@ -1,4 +1,4 @@
1
- import {Name, TimePoint, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
1
+ import {Name, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
2
2
  import {ServerContract} from '../contracts'
3
3
  import {Coordinates, TaskType} from '../types'
4
4
  import {
@@ -333,50 +333,27 @@ export interface ProjectionOptions {
333
333
 
334
334
  export function projectEntity(entity: Projectable, options?: ProjectionOptions): ProjectedEntity {
335
335
  const projected = createProjectedEntity(entity)
336
- if (!entity.schedule || entity.schedule.tasks.length === 0) return projected
336
+ const ordered = schedule.orderedTasks(entity)
337
+ if (ordered.length === 0) return projected
337
338
 
338
- const tasks = entity.schedule.tasks
339
339
  const taskCount =
340
340
  options?.upToTaskIndex !== undefined
341
- ? Math.max(0, Math.min(options.upToTaskIndex, tasks.length))
342
- : tasks.length
341
+ ? Math.max(0, Math.min(options.upToTaskIndex, ordered.length))
342
+ : ordered.length
343
343
 
344
344
  for (let i = 0; i < taskCount; i++) {
345
- applyTask(projected, tasks[i])
345
+ applyTask(projected, ordered[i].task)
346
346
  }
347
347
  return projected
348
348
  }
349
349
 
350
- export interface ProjectableSnapshot extends Projectable {
351
- current_task?: ServerContract.Types.task
352
- pending_tasks?: ServerContract.Types.task[]
353
- }
354
-
355
- function buildRemainingProjectable(snapshot: ProjectableSnapshot): Projectable | null {
356
- if (!snapshot.schedule) return null
357
- const remainingTasks: ServerContract.Types.task[] = []
358
- if (snapshot.current_task) remainingTasks.push(snapshot.current_task)
359
- if (snapshot.pending_tasks?.length) remainingTasks.push(...snapshot.pending_tasks)
360
- if (remainingTasks.length === 0) return null
361
-
362
- const completedCount = snapshot.schedule.tasks.length - remainingTasks.length
363
- let startedMs = snapshot.schedule.started.toMilliseconds()
364
- for (let i = 0; i < completedCount; i++) {
365
- startedMs += snapshot.schedule.tasks[i].duration.toNumber() * 1000
366
- }
367
-
368
- return {
369
- ...snapshot,
370
- schedule: ServerContract.Types.schedule.from({
371
- started: TimePoint.fromMilliseconds(startedMs),
372
- tasks: remainingTasks,
373
- }),
350
+ export function projectRemainingAt(entity: Projectable, _now: Date): ProjectedEntity {
351
+ // Resolve is lazy/entity-global: completed tasks are unsettled until resolve, so replay all.
352
+ const projected = createProjectedEntity(entity)
353
+ for (const {task} of schedule.orderedTasks(entity)) {
354
+ applyTask(projected, task)
374
355
  }
375
- }
376
-
377
- export function projectFromCurrentState(snapshot: ProjectableSnapshot): ProjectedEntity {
378
- const projectable = buildRemainingProjectable(snapshot)
379
- return projectable ? projectEntity(projectable) : createProjectedEntity(snapshot)
356
+ return projected
380
357
  }
381
358
 
382
359
  function getRecipeInputsForOutput(outputItemId: number): RecipeInput[] | undefined {
@@ -438,10 +415,11 @@ function validateCraftTask(task: ServerContract.Types.task, projected: Projected
438
415
  }
439
416
 
440
417
  export function validateSchedule(entity: Projectable): void {
441
- if (!entity.schedule || entity.schedule.tasks.length === 0) return
418
+ const ordered = schedule.orderedTasks(entity)
419
+ if (ordered.length === 0) return
442
420
 
443
421
  const projected = createProjectedEntity(entity)
444
- for (const task of entity.schedule.tasks) {
422
+ for (const {task} of ordered) {
445
423
  if (task.type.toNumber() === TaskType.CRAFT) {
446
424
  validateCraftTask(task, projected)
447
425
  }
@@ -455,22 +433,28 @@ export function validateSchedule(entity: Projectable): void {
455
433
  export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity {
456
434
  const projected = createProjectedEntity(entity)
457
435
 
458
- if (!entity.schedule || entity.schedule.tasks.length === 0) {
436
+ const ordered = schedule.orderedTasks(entity)
437
+ if (ordered.length === 0) {
459
438
  return projected
460
439
  }
461
440
 
462
- for (let i = 0; i < entity.schedule.tasks.length; i++) {
463
- const task = entity.schedule.tasks[i]
464
- const taskComplete = schedule.isTaskComplete(entity, i, now)
465
- const taskInProgress = schedule.isTaskInProgress(entity, i, now)
441
+ const nowMs = now.getTime()
442
+
443
+ for (const {task, startsAt} of ordered) {
444
+ const duration = task.duration.toNumber()
445
+ const isReserved = task.type.toNumber() === TaskType.RESERVED
446
+ const elapsed = Math.min(
447
+ Math.max(0, Math.floor((nowMs - startsAt.getTime()) / 1000)),
448
+ duration
449
+ )
450
+ const taskComplete = !isReserved && elapsed >= duration
451
+ const taskInProgress = elapsed > 0 && elapsed < duration
466
452
 
467
453
  if (!taskComplete && !taskInProgress) {
468
- break
454
+ continue
469
455
  }
470
456
 
471
- const progress = taskInProgress
472
- ? schedule.getTaskElapsed(entity, i, now) / task.duration.toNumber()
473
- : undefined
457
+ const progress = taskInProgress ? elapsed / duration : undefined
474
458
 
475
459
  switch (task.type.toNumber()) {
476
460
  case TaskType.RECHARGE:
@@ -504,11 +488,3 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
504
488
 
505
489
  return projected
506
490
  }
507
-
508
- export function projectFromCurrentStateAt(
509
- snapshot: ProjectableSnapshot,
510
- now: Date
511
- ): ProjectedEntity {
512
- const projectable = buildRemainingProjectable(snapshot)
513
- return projectable ? projectEntityAt(projectable, now) : createProjectedEntity(snapshot)
514
- }
@@ -1,198 +1,313 @@
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
+
9
+ export const LANE_MOBILITY = 0
10
+ export const LANE_BARRIER = 255
6
11
 
7
12
  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
13
+ lanes?: Lane[]
14
+ }
15
+
16
+ export interface LaneView {
17
+ laneKey: number
18
+ schedule: Schedule
19
+ }
20
+
21
+ export {
22
+ laneStartsIn,
23
+ currentTaskIndexForLane,
24
+ laneTaskComplete,
25
+ laneTaskInProgress,
26
+ laneCompletesAt,
27
+ currentTaskProgressFloatForLane,
28
+ } from './lane-core'
29
+
30
+ export function getLanes(entity: ScheduleData): LaneView[] {
31
+ const lanes = entity.lanes
32
+ if (!lanes || lanes.length === 0) return []
33
+ return lanes.map((l) => ({laneKey: l.lane_key.toNumber(), schedule: l.schedule}))
34
+ }
35
+
36
+ export function getLane(entity: ScheduleData, laneKey: number): LaneView | undefined {
37
+ const lanes = entity.lanes
38
+ if (!lanes) return undefined
39
+ for (const l of lanes) {
40
+ if (l.lane_key.toNumber() === laneKey) return {laneKey, schedule: l.schedule}
41
+ }
42
+ return undefined
43
+ }
44
+
45
+ export function mobilityLane(entity: ScheduleData): LaneView | undefined {
46
+ return getLane(entity, LANE_MOBILITY)
29
47
  }
30
48
 
31
49
  export function hasSchedule(entity: ScheduleData): boolean {
32
- return !!entity.schedule && entity.schedule.tasks.length > 0
50
+ const lanes = entity.lanes
51
+ if (!lanes) return false
52
+ return lanes.some((l) => l.schedule.tasks.length > 0)
33
53
  }
34
54
 
35
55
  export function isIdle(entity: ScheduleData): boolean {
36
56
  return !hasSchedule(entity)
37
57
  }
38
58
 
59
+ export function isEntityIdle(entity: ScheduleData, now: Date): boolean {
60
+ const lanes = entity.lanes
61
+ if (!lanes) return true
62
+ return lanes.every((l) => core.currentTaskIndexForLane(l.schedule, now) < 0)
63
+ }
64
+
65
+ export function entityIdleAt(entity: ScheduleData, _now: Date): Date | undefined {
66
+ const lanes = entity.lanes
67
+ if (!lanes) return undefined
68
+ let maxMs: number | undefined
69
+ for (const l of lanes) {
70
+ if (l.schedule.tasks.length === 0) continue
71
+ const endMs = l.schedule.started.toDate().getTime() + core.laneDuration(l.schedule) * 1000
72
+ if (maxMs === undefined || endMs > maxMs) maxMs = endMs
73
+ }
74
+ return maxMs === undefined ? undefined : new Date(maxMs)
75
+ }
76
+
39
77
  export function getTasks(entity: ScheduleData): Task[] {
40
- return entity.schedule?.tasks || []
78
+ const lanes = entity.lanes
79
+ if (!lanes) return []
80
+ return lanes.flatMap((l) => l.schedule.tasks)
41
81
  }
42
82
 
43
83
  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)
84
+ let max = 0
85
+ for (const l of entity.lanes ?? []) max = Math.max(max, core.laneDuration(l.schedule))
86
+ return max
46
87
  }
47
88
 
48
89
  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)
90
+ let max = 0
91
+ for (const l of entity.lanes ?? []) max = Math.max(max, core.laneElapsed(l.schedule, now))
92
+ return max
53
93
  }
54
94
 
55
95
  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)
96
+ let remaining = 0
97
+ for (const l of entity.lanes ?? []) {
98
+ remaining = Math.max(remaining, core.laneRemaining(l.schedule, now))
99
+ }
100
+ return remaining
60
101
  }
61
102
 
62
103
  export function scheduleComplete(entity: ScheduleData, now: Date): boolean {
63
- if (!hasSchedule(entity)) return false
64
- if ((entity.schedule?.tasks ?? []).some((t) => t.type.toNumber() === TaskType.RESERVED))
65
- return false
66
- return scheduleRemaining(entity, now) === 0
67
- }
68
-
69
- export function currentTaskIndex(entity: ScheduleData, now: Date): number {
70
- if (!entity.schedule || entity.schedule.tasks.length === 0) return -1
71
-
72
- const elapsed = scheduleElapsed(entity, now)
73
- let timeAccum = 0
74
-
75
- for (let i = 0; i < entity.schedule.tasks.length; i++) {
76
- const taskDuration = entity.schedule.tasks[i].duration.toNumber()
77
- if (elapsed < timeAccum + taskDuration) {
78
- return i
104
+ const lanes = entity.lanes
105
+ if (!lanes) return false
106
+ let hasAnyTask = false
107
+ let remaining = 0
108
+ for (const l of lanes) {
109
+ const tasks = l.schedule.tasks
110
+ if (tasks.length > 0) {
111
+ hasAnyTask = true
112
+ for (const t of tasks) {
113
+ if (t.type.toNumber() === TaskType.RESERVED) return false
114
+ }
79
115
  }
80
- timeAccum += taskDuration
116
+ remaining = Math.max(remaining, core.laneRemaining(l.schedule, now))
81
117
  }
118
+ if (!hasAnyTask) return false
119
+ return remaining === 0
120
+ }
82
121
 
83
- return -1
122
+ // Mirrors contract lane_front_complete: any lane whose front task is complete and non-reserved.
123
+ export function hasResolvable(entity: ScheduleData, now: Date): boolean {
124
+ for (const l of entity.lanes ?? []) {
125
+ if (core.laneTaskComplete(l.schedule, 0, now)) return true
126
+ }
127
+ return false
84
128
  }
85
129
 
86
- export function currentTask(entity: ScheduleData, now: Date): Task | undefined {
87
- const index = currentTaskIndex(entity, now)
88
- if (index < 0 || !entity.schedule) return undefined
89
- return entity.schedule.tasks[index]
130
+ export function currentTaskForLane(
131
+ entity: ScheduleData,
132
+ laneKey: number,
133
+ now: Date
134
+ ): Task | undefined {
135
+ const lane = getLane(entity, laneKey)
136
+ return lane ? core.currentTask(lane.schedule, now) : undefined
90
137
  }
91
138
 
92
- export function currentTaskType(entity: ScheduleData, now: Date): TaskType | undefined {
93
- const task = currentTask(entity, now)
94
- if (!task) return undefined
95
- return task.type.toNumber() as TaskType
139
+ export function currentTaskTypeForLane(
140
+ entity: ScheduleData,
141
+ laneKey: number,
142
+ now: Date
143
+ ): TaskType | undefined {
144
+ const lane = getLane(entity, laneKey)
145
+ return lane ? core.currentTaskType(lane.schedule, now) : undefined
96
146
  }
97
147
 
98
- export function getTaskStartTime(entity: ScheduleData, index: number): number {
99
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return 0
100
- let timeAccum = 0
101
- for (let i = 0; i < index; i++) {
102
- timeAccum += entity.schedule.tasks[i].duration.toNumber()
148
+ export function activeTasks(entity: ScheduleData, now: Date): Task[] {
149
+ const out: Task[] = []
150
+ for (const l of entity.lanes ?? []) {
151
+ const idx = core.currentTaskIndexForLane(l.schedule, now)
152
+ if (idx >= 0) out.push(l.schedule.tasks[idx])
103
153
  }
104
- return timeAccum
154
+ return out
105
155
  }
106
156
 
107
- export function getTaskElapsed(entity: ScheduleData, index: number, now: Date): number {
108
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return 0
157
+ export interface ResolvedEvent {
158
+ laneKey: number
159
+ taskIndex: number
160
+ task: Task
161
+ completesAt: Date
162
+ }
109
163
 
110
- const elapsed = scheduleElapsed(entity, now)
111
- const taskStart = getTaskStartTime(entity, index)
112
- const taskDuration = entity.schedule.tasks[index].duration.toNumber()
164
+ // Canonical lane-front order (mirrors contract front_precedes): completion, then RECHARGE-last, then lane key.
165
+ function frontPrecedes(
166
+ a: {completesAt: Date; task: Task; laneKey: number},
167
+ b: {completesAt: Date; task: Task; laneKey: number}
168
+ ): number {
169
+ if (a.completesAt.getTime() !== b.completesAt.getTime()) {
170
+ return a.completesAt.getTime() - b.completesAt.getTime()
171
+ }
172
+ const aRecharge = a.task.type.toNumber() === TaskType.RECHARGE
173
+ const bRecharge = b.task.type.toNumber() === TaskType.RECHARGE
174
+ if (aRecharge !== bRecharge) return aRecharge ? 1 : -1
175
+ return a.laneKey - b.laneKey
176
+ }
113
177
 
114
- if (elapsed <= taskStart) return 0
115
- const elapsedInTask = elapsed - taskStart
116
- return Math.min(elapsedInTask, taskDuration)
178
+ // Completed lane-fronts in canonical order (mirrors contract front_precedes).
179
+ export function resolveOrder(entity: ScheduleData, now: Date): ResolvedEvent[] {
180
+ const events: ResolvedEvent[] = []
181
+ for (const l of entity.lanes ?? []) {
182
+ const laneKey = l.lane_key.toNumber()
183
+ const startedMs = l.schedule.started.toDate().getTime()
184
+ let endSec = 0
185
+ for (let i = 0; i < l.schedule.tasks.length; i++) {
186
+ const task = l.schedule.tasks[i]
187
+ endSec += task.duration.toNumber()
188
+ const completesAt = new Date(startedMs + endSec * 1000)
189
+ if (task.type.toNumber() === TaskType.RESERVED) break
190
+ if (completesAt.getTime() > now.getTime()) break
191
+ events.push({laneKey, taskIndex: i, task, completesAt})
192
+ }
193
+ }
194
+ events.sort(frontPrecedes)
195
+ return events
117
196
  }
118
197
 
119
- export function getTaskRemaining(entity: ScheduleData, index: number, now: Date): number {
120
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return 0
198
+ export interface OrderedTask {
199
+ laneKey: number
200
+ taskIndex: number
201
+ task: Task
202
+ startsAt: Date
203
+ completesAt: Date
204
+ }
205
+
206
+ // Every task across all lanes in canonical order (mirrors contract front_precedes).
207
+ export function orderedTasks(entity: ScheduleData): OrderedTask[] {
208
+ const out: OrderedTask[] = []
209
+ for (const l of entity.lanes ?? []) {
210
+ const laneKey = l.lane_key.toNumber()
211
+ const startedMs = l.schedule.started.toDate().getTime()
212
+ let endSec = 0
213
+ for (let i = 0; i < l.schedule.tasks.length; i++) {
214
+ const task = l.schedule.tasks[i]
215
+ const startsAt = new Date(startedMs + endSec * 1000)
216
+ endSec += task.duration.toNumber()
217
+ const completesAt = new Date(startedMs + endSec * 1000)
218
+ out.push({laneKey, taskIndex: i, task, startsAt, completesAt})
219
+ }
220
+ }
221
+ out.sort(frontPrecedes)
222
+ return out
223
+ }
121
224
 
122
- const taskDuration = entity.schedule.tasks[index].duration.toNumber()
123
- const taskElapsed = getTaskElapsed(entity, index, now)
124
- return Math.max(0, taskDuration - taskElapsed)
225
+ export function laneRemainingOf(entity: ScheduleData, laneKey: number, now: Date): number {
226
+ const lane = getLane(entity, laneKey)
227
+ return lane ? core.laneRemaining(lane.schedule, now) : 0
125
228
  }
126
229
 
127
- export function isTaskComplete(entity: ScheduleData, index: number, now: Date): boolean {
128
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return false
230
+ export function laneStartsInOf(entity: ScheduleData, laneKey: number, now: Date): number {
231
+ const lane = getLane(entity, laneKey)
232
+ return lane ? core.laneStartsIn(lane.schedule, now) : 0
233
+ }
129
234
 
130
- if (entity.schedule.tasks[index].type.toNumber() === TaskType.RESERVED) return false
235
+ export function laneCompleteOf(entity: ScheduleData, laneKey: number, now: Date): boolean {
236
+ const lane = getLane(entity, laneKey)
237
+ return lane ? core.laneComplete(lane.schedule, now) : false
238
+ }
131
239
 
132
- const taskDuration = entity.schedule.tasks[index].duration.toNumber()
133
- const taskElapsed = getTaskElapsed(entity, index, now)
134
- return taskElapsed >= taskDuration
240
+ export function laneProgressOf(entity: ScheduleData, laneKey: number, now: Date): number {
241
+ const lane = getLane(entity, laneKey)
242
+ return lane ? core.laneProgress(lane.schedule, now) : 0
135
243
  }
136
244
 
137
- export function isTaskInProgress(entity: ScheduleData, index: number, now: Date): boolean {
138
- if (!entity.schedule || index < 0 || index >= entity.schedule.tasks.length) return false
245
+ export function laneTaskElapsedOf(
246
+ entity: ScheduleData,
247
+ laneKey: number,
248
+ index: number,
249
+ now: Date
250
+ ): number {
251
+ const lane = getLane(entity, laneKey)
252
+ return lane ? core.laneTaskElapsed(lane.schedule, index, now) : 0
253
+ }
139
254
 
140
- const taskElapsed = getTaskElapsed(entity, index, now)
141
- const taskDuration = entity.schedule.tasks[index].duration.toNumber()
142
- return taskElapsed > 0 && taskElapsed < taskDuration
255
+ export function laneTaskRemainingOf(
256
+ entity: ScheduleData,
257
+ laneKey: number,
258
+ index: number,
259
+ now: Date
260
+ ): number {
261
+ const lane = getLane(entity, laneKey)
262
+ return lane ? core.laneTaskRemaining(lane.schedule, index, now) : 0
143
263
  }
144
264
 
145
- export function currentTaskProgress(entity: ScheduleData, now: Date): number {
146
- const task = currentTask(entity, now)
147
- if (!task) return 0
148
- const index = currentTaskIndex(entity, now)
149
- const elapsed = getTaskElapsed(entity, index, now)
150
- const duration = task.duration.toNumber()
151
- if (duration === 0) return 1
152
- return Math.min(1, elapsed / duration)
265
+ export function laneTaskCompleteOf(
266
+ entity: ScheduleData,
267
+ laneKey: number,
268
+ index: number,
269
+ now: Date
270
+ ): boolean {
271
+ const lane = getLane(entity, laneKey)
272
+ return lane ? core.laneTaskComplete(lane.schedule, index, now) : false
153
273
  }
154
274
 
155
- export function currentTaskProgressFloat(entity: ScheduleData, now: Date): number {
156
- if (!entity.schedule || entity.schedule.tasks.length === 0) return 0
157
- const index = currentTaskIndex(entity, now)
158
- if (index < 0) return 0
159
- const task = entity.schedule.tasks[index]
160
- const durationMs = task.duration.toNumber() * 1000
161
- if (durationMs === 0) return 1
162
- const startedMs = entity.schedule.started.toDate().getTime()
163
- const taskStartMs = startedMs + getTaskStartTime(entity, index) * 1000
164
- const elapsedMs = now.getTime() - taskStartMs
165
- if (elapsedMs <= 0) return 0
166
- return Math.min(1, elapsedMs / durationMs)
275
+ export function laneTaskInProgressOf(
276
+ entity: ScheduleData,
277
+ laneKey: number,
278
+ index: number,
279
+ now: Date
280
+ ): boolean {
281
+ const lane = getLane(entity, laneKey)
282
+ return lane ? core.laneTaskInProgress(lane.schedule, index, now) : false
167
283
  }
168
284
 
169
- export function scheduleProgress(entity: ScheduleData, now: Date): number {
170
- const duration = scheduleDuration(entity)
171
- if (duration === 0) return hasSchedule(entity) ? 1 : 0
172
- const elapsed = scheduleElapsed(entity, now)
173
- return Math.min(1, elapsed / duration)
285
+ export function currentTaskIndexOf(entity: ScheduleData, laneKey: number, now: Date): number {
286
+ const lane = getLane(entity, laneKey)
287
+ return lane ? core.currentTaskIndexForLane(lane.schedule, now) : -1
174
288
  }
175
289
 
176
- export function isTaskType(entity: ScheduleData, taskType: TaskType, now: Date): boolean {
177
- return currentTaskType(entity, now) === taskType
290
+ function entityDoesTaskType(entity: ScheduleData, taskType: TaskType, now: Date): boolean {
291
+ return activeTasks(entity, now).some((t) => t.type.toNumber() === taskType)
178
292
  }
179
293
 
180
294
  export function isInFlight(entity: ScheduleData, now: Date): boolean {
181
- return isTaskType(entity, TaskType.TRAVEL, now)
295
+ const lane = mobilityLane(entity)
296
+ return lane ? core.currentTaskType(lane.schedule, now) === TaskType.TRAVEL : false
182
297
  }
183
298
 
184
299
  export function isRecharging(entity: ScheduleData, now: Date): boolean {
185
- return isTaskType(entity, TaskType.RECHARGE, now)
300
+ return entityDoesTaskType(entity, TaskType.RECHARGE, now)
186
301
  }
187
302
 
188
303
  export function isLoading(entity: ScheduleData, now: Date): boolean {
189
- return isTaskType(entity, TaskType.LOAD, now)
304
+ return entityDoesTaskType(entity, TaskType.LOAD, now)
190
305
  }
191
306
 
192
307
  export function isUnloading(entity: ScheduleData, now: Date): boolean {
193
- return isTaskType(entity, TaskType.UNLOAD, now)
308
+ return entityDoesTaskType(entity, TaskType.UNLOAD, now)
194
309
  }
195
310
 
196
311
  export function isGathering(entity: ScheduleData, now: Date): boolean {
197
- return isTaskType(entity, TaskType.GATHER, now)
312
+ return entityDoesTaskType(entity, TaskType.GATHER, now)
198
313
  }