@shipload/sdk 1.0.0-next.73 → 1.0.0-next.75
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/shipload.d.ts +514 -127
- package/lib/shipload.js +5952 -4948
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +5936 -4942
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +174 -47
- package/lib/testing.js +539 -147
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +539 -147
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/contracts/server.ts +457 -134
- package/src/data/component-process.test.ts +119 -0
- package/src/data/component-process.ts +33 -0
- package/src/data/entities.json +2 -2
- package/src/data/metadata.ts +18 -5
- package/src/index-module.ts +29 -2
- package/src/influence/civic-shuttle.test.ts +58 -0
- package/src/influence/civic-shuttle.ts +52 -6
- package/src/influence/duration.ts +0 -65
- package/src/influence/index.ts +4 -5
- package/src/influence/influence.test.ts +51 -136
- package/src/influence/valuation.ts +75 -0
- package/src/managers/actions.ts +37 -10
- package/src/managers/balances.test.ts +33 -0
- package/src/managers/balances.ts +15 -0
- package/src/managers/context.ts +50 -11
- package/src/managers/index.ts +14 -0
- package/src/managers/jobs.test.ts +82 -1
- package/src/managers/jobs.ts +41 -2
- package/src/managers/nft.test.ts +173 -1
- package/src/managers/nft.ts +35 -2
- package/src/managers/shuttle.test.ts +562 -0
- package/src/managers/shuttle.ts +397 -0
- package/src/resolution/resolve-item.ts +4 -0
- package/src/scheduling/availability.ts +10 -0
- package/src/scheduling/energy.ts +55 -1
- package/src/scheduling/jobs.test.ts +171 -1
- package/src/scheduling/jobs.ts +110 -13
- package/src/scheduling/projection.ts +2 -1
- package/src/scheduling/task-effects.ts +38 -0
- package/src/shipload.ts +23 -4
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
ITEM_BEAM,
|
|
4
|
+
ITEM_ENGINE_T1,
|
|
5
|
+
ITEM_ORE_T1,
|
|
6
|
+
ITEM_PLATE,
|
|
7
|
+
ITEM_PLATE_T2,
|
|
8
|
+
ITEM_PROSPECTOR_T1A_PACKED,
|
|
9
|
+
ITEM_PROSPECTOR_T2A_PACKED,
|
|
10
|
+
ITEM_PROSPECTOR_T2B_PACKED,
|
|
11
|
+
ITEM_ROUSTABOUT_T1A_PACKED,
|
|
12
|
+
ITEM_STORAGE_T1,
|
|
13
|
+
} from './item-ids'
|
|
14
|
+
import {getComponents} from './catalog'
|
|
15
|
+
import {getRecipe} from './recipes-runtime'
|
|
16
|
+
import {completeComponentSet, getComponentProcess} from './component-process'
|
|
17
|
+
import {resolveItem} from '../resolution/resolve-item'
|
|
18
|
+
|
|
19
|
+
function inputsOf(itemId: number) {
|
|
20
|
+
const recipe = getRecipe(itemId)
|
|
21
|
+
if (!recipe) throw new Error(`no recipe for ${itemId}`)
|
|
22
|
+
return recipe.inputs
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('component process', () => {
|
|
26
|
+
test('every component at every tier is refined or machined, five of each per tier', () => {
|
|
27
|
+
const tiers = new Set(getComponents().map((item) => item.tier))
|
|
28
|
+
for (const tier of tiers) {
|
|
29
|
+
const kinds = getComponents({tier}).map((item) => getComponentProcess(item.id))
|
|
30
|
+
expect(kinds.filter((k) => k === 'refined').length).toBe(5)
|
|
31
|
+
expect(kinds.filter((k) => k === 'machined').length).toBe(5)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('the refined five are the single-resource components', () => {
|
|
36
|
+
const refined = getComponents({tier: 1})
|
|
37
|
+
.filter((item) => getComponentProcess(item.id) === 'refined')
|
|
38
|
+
.map((item) => item.name)
|
|
39
|
+
.sort()
|
|
40
|
+
expect(refined).toEqual(['Ceramic', 'Plasma Cell', 'Plate', 'Polymer', 'Sensor'])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('the kind holds across tiers', () => {
|
|
44
|
+
expect(getComponentProcess(ITEM_PLATE)).toBe('refined')
|
|
45
|
+
expect(getComponentProcess(ITEM_PLATE_T2)).toBe('refined')
|
|
46
|
+
expect(getComponentProcess(ITEM_BEAM)).toBe('machined')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('non-components and unknown ids have no kind', () => {
|
|
50
|
+
expect(getComponentProcess(ITEM_ORE_T1)).toBeUndefined()
|
|
51
|
+
expect(getComponentProcess(ITEM_ENGINE_T1)).toBeUndefined()
|
|
52
|
+
expect(getComponentProcess(ITEM_ROUSTABOUT_T1A_PACKED)).toBeUndefined()
|
|
53
|
+
expect(getComponentProcess(65535)).toBeUndefined()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('resolveItem carries the kind for components only', () => {
|
|
57
|
+
expect(resolveItem(ITEM_BEAM).process).toBe('machined')
|
|
58
|
+
expect(resolveItem(ITEM_PLATE_T2).process).toBe('refined')
|
|
59
|
+
expect(resolveItem(ITEM_ORE_T1).process).toBeUndefined()
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('completeComponentSet', () => {
|
|
64
|
+
test('recognises the hull First and Second step recipes', () => {
|
|
65
|
+
expect(completeComponentSet(inputsOf(ITEM_ROUSTABOUT_T1A_PACKED))).toEqual({
|
|
66
|
+
process: 'refined',
|
|
67
|
+
tier: 1,
|
|
68
|
+
quantity: 200,
|
|
69
|
+
})
|
|
70
|
+
expect(completeComponentSet(inputsOf(ITEM_PROSPECTOR_T1A_PACKED))).toEqual({
|
|
71
|
+
process: 'machined',
|
|
72
|
+
tier: 1,
|
|
73
|
+
quantity: 100,
|
|
74
|
+
})
|
|
75
|
+
expect(completeComponentSet(inputsOf(ITEM_PROSPECTOR_T2A_PACKED))).toEqual({
|
|
76
|
+
process: 'refined',
|
|
77
|
+
tier: 2,
|
|
78
|
+
quantity: 200,
|
|
79
|
+
})
|
|
80
|
+
expect(completeComponentSet(inputsOf(ITEM_PROSPECTOR_T2B_PACKED))).toEqual({
|
|
81
|
+
process: 'machined',
|
|
82
|
+
tier: 2,
|
|
83
|
+
quantity: 150,
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('a mixed module recipe is not a complete set', () => {
|
|
88
|
+
expect(completeComponentSet(inputsOf(ITEM_STORAGE_T1))).toBeNull()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('unequal quantities keep the set but drop the shared quantity', () => {
|
|
92
|
+
const inputs = inputsOf(ITEM_ROUSTABOUT_T1A_PACKED).map((input, index) =>
|
|
93
|
+
index === 0 ? {...input, quantity: 50} : input
|
|
94
|
+
)
|
|
95
|
+
expect(completeComponentSet(inputs)).toEqual({process: 'refined', tier: 1, quantity: null})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('one machined part among four refined is not a set', () => {
|
|
99
|
+
const inputs = inputsOf(ITEM_ROUSTABOUT_T1A_PACKED).map((input, index) =>
|
|
100
|
+
index === 0 ? {...input, itemId: ITEM_BEAM} : input
|
|
101
|
+
)
|
|
102
|
+
expect(completeComponentSet(inputs)).toBeNull()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('a repeated family is not a set', () => {
|
|
106
|
+
const inputs = inputsOf(ITEM_ROUSTABOUT_T1A_PACKED).map((input) => ({
|
|
107
|
+
...input,
|
|
108
|
+
itemId: ITEM_PLATE,
|
|
109
|
+
}))
|
|
110
|
+
expect(completeComponentSet(inputs)).toBeNull()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('mixed tiers are not a set', () => {
|
|
114
|
+
const inputs = inputsOf(ITEM_ROUSTABOUT_T1A_PACKED).map((input, index) =>
|
|
115
|
+
index === 0 ? {...input, itemId: ITEM_PLATE_T2} : input
|
|
116
|
+
)
|
|
117
|
+
expect(completeComponentSet(inputs)).toBeNull()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {getItem} from './catalog'
|
|
2
|
+
import {getItemFamily, type ComponentProcess} from './metadata'
|
|
3
|
+
import type {RecipeInput} from './recipes-runtime'
|
|
4
|
+
|
|
5
|
+
export const COMPONENT_PROCESS_ORDER: readonly ComponentProcess[] = ['refined', 'machined']
|
|
6
|
+
|
|
7
|
+
export function getComponentProcess(itemId: number): ComponentProcess | undefined {
|
|
8
|
+
return getItemFamily(itemId)?.process
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CompleteComponentSet {
|
|
12
|
+
process: ComponentProcess
|
|
13
|
+
tier: number
|
|
14
|
+
quantity: number | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function completeComponentSet(inputs: readonly RecipeInput[]): CompleteComponentSet | null {
|
|
18
|
+
const parts = inputs.filter((input) => getComponentProcess(input.itemId) !== undefined)
|
|
19
|
+
if (parts.length !== 5) return null
|
|
20
|
+
const process = getComponentProcess(parts[0].itemId)
|
|
21
|
+
const tier = getItem(parts[0].itemId).tier
|
|
22
|
+
const families = new Set<string>()
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
const item = getItem(part.itemId)
|
|
25
|
+
if (getComponentProcess(part.itemId) !== process || item.tier !== tier) return null
|
|
26
|
+
families.add(item.family)
|
|
27
|
+
}
|
|
28
|
+
if (!process || families.size !== 5) return null
|
|
29
|
+
const quantity = parts.every((part) => part.quantity === parts[0].quantity)
|
|
30
|
+
? parts[0].quantity
|
|
31
|
+
: null
|
|
32
|
+
return {process, tier, quantity}
|
|
33
|
+
}
|
package/src/data/entities.json
CHANGED
package/src/data/metadata.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import items from './items.json'
|
|
2
2
|
|
|
3
|
+
export type ComponentProcess = 'refined' | 'machined'
|
|
4
|
+
|
|
3
5
|
export interface ItemFamily {
|
|
4
6
|
name: string
|
|
5
7
|
description: string
|
|
6
8
|
color: string
|
|
9
|
+
process?: ComponentProcess
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export interface EntityMetadata {
|
|
@@ -14,27 +17,27 @@ export const itemFamilies: Record<string, ItemFamily> = {
|
|
|
14
17
|
// === Resources ===
|
|
15
18
|
ore: {
|
|
16
19
|
name: 'Ore',
|
|
17
|
-
description: '
|
|
20
|
+
description: 'Goes into Plate and Beam.',
|
|
18
21
|
color: '#C26D3F',
|
|
19
22
|
},
|
|
20
23
|
crystal: {
|
|
21
24
|
name: 'Crystal',
|
|
22
|
-
description: '
|
|
25
|
+
description: 'Goes into Sensors and Resonators.',
|
|
23
26
|
color: '#4ADBFF',
|
|
24
27
|
},
|
|
25
28
|
gas: {
|
|
26
29
|
name: 'Gas',
|
|
27
|
-
description: '
|
|
30
|
+
description: 'Goes into Plasma Cells and Reactors.',
|
|
28
31
|
color: '#B877FF',
|
|
29
32
|
},
|
|
30
33
|
regolith: {
|
|
31
34
|
name: 'Regolith',
|
|
32
|
-
description: '
|
|
35
|
+
description: 'Goes into Ceramic and Frame.',
|
|
33
36
|
color: '#C4A57B',
|
|
34
37
|
},
|
|
35
38
|
biomass: {
|
|
36
39
|
name: 'Biomass',
|
|
37
|
-
description: '
|
|
40
|
+
description: 'Goes into Polymer and Resin.',
|
|
38
41
|
color: '#5A8B3E',
|
|
39
42
|
},
|
|
40
43
|
|
|
@@ -43,51 +46,61 @@ export const itemFamilies: Record<string, ItemFamily> = {
|
|
|
43
46
|
name: 'Plate',
|
|
44
47
|
description: 'Goes into Cargo Holds, Containers, and hulls.',
|
|
45
48
|
color: '#7B8D9E',
|
|
49
|
+
process: 'refined',
|
|
46
50
|
},
|
|
47
51
|
frame: {
|
|
48
52
|
name: 'Frame',
|
|
49
53
|
description: 'Goes into Limpet Bays, Cargo Holds, and hulls.',
|
|
50
54
|
color: '#C4A57B',
|
|
55
|
+
process: 'machined',
|
|
51
56
|
},
|
|
52
57
|
'plasma-cell': {
|
|
53
58
|
name: 'Plasma Cell',
|
|
54
59
|
description: 'Goes into Engines, Battery Banks, and hulls.',
|
|
55
60
|
color: '#E86344',
|
|
61
|
+
process: 'refined',
|
|
56
62
|
},
|
|
57
63
|
resonator: {
|
|
58
64
|
name: 'Resonator',
|
|
59
65
|
description: 'Goes into Power Cores, Tractor Beams, Warp Drives, and hulls.',
|
|
60
66
|
color: '#4ADBFF',
|
|
67
|
+
process: 'machined',
|
|
61
68
|
},
|
|
62
69
|
beam: {
|
|
63
70
|
name: 'Beam',
|
|
64
71
|
description: 'Goes into Limpet Bays and hulls.',
|
|
65
72
|
color: '#7B8D9E',
|
|
73
|
+
process: 'machined',
|
|
66
74
|
},
|
|
67
75
|
sensor: {
|
|
68
76
|
name: 'Sensor',
|
|
69
77
|
description: 'Goes into Fabricators, Assembly Arms, and hulls.',
|
|
70
78
|
color: '#4ADBFF',
|
|
79
|
+
process: 'refined',
|
|
71
80
|
},
|
|
72
81
|
polymer: {
|
|
73
82
|
name: 'Polymer',
|
|
74
83
|
description: 'Goes into Shuttle Bays, Battery Banks, and hulls.',
|
|
75
84
|
color: '#5A8B3E',
|
|
85
|
+
process: 'refined',
|
|
76
86
|
},
|
|
77
87
|
ceramic: {
|
|
78
88
|
name: 'Ceramic',
|
|
79
89
|
description: 'Goes into Assembly Arms, Containers, and hulls.',
|
|
80
90
|
color: '#C4A57B',
|
|
91
|
+
process: 'refined',
|
|
81
92
|
},
|
|
82
93
|
reactor: {
|
|
83
94
|
name: 'Reactor',
|
|
84
95
|
description: 'Goes into Fabricators, Warp Drives, and hulls.',
|
|
85
96
|
color: '#B877FF',
|
|
97
|
+
process: 'machined',
|
|
86
98
|
},
|
|
87
99
|
resin: {
|
|
88
100
|
name: 'Resin',
|
|
89
101
|
description: 'Goes into Tractor Beams and hulls.',
|
|
90
102
|
color: '#5A8B3E',
|
|
103
|
+
process: 'machined',
|
|
91
104
|
},
|
|
92
105
|
|
|
93
106
|
// === Modules ===
|
package/src/index-module.ts
CHANGED
|
@@ -62,6 +62,22 @@ export {
|
|
|
62
62
|
JobsManager,
|
|
63
63
|
InfluenceManager,
|
|
64
64
|
} from './managers'
|
|
65
|
+
export {
|
|
66
|
+
ShuttleManager,
|
|
67
|
+
rankShuttleOptions,
|
|
68
|
+
shuttleReasonCode,
|
|
69
|
+
shuttleCandidates,
|
|
70
|
+
BOOKING_LEVEL_CODES,
|
|
71
|
+
OMITTED_REASONS,
|
|
72
|
+
} from './managers/shuttle'
|
|
73
|
+
export type {
|
|
74
|
+
ShuttleMode,
|
|
75
|
+
ShuttleOption,
|
|
76
|
+
ShuttleOptions,
|
|
77
|
+
ShuttleReason,
|
|
78
|
+
ShuttleReasonCode,
|
|
79
|
+
CandidateEntity,
|
|
80
|
+
} from './managers/shuttle'
|
|
65
81
|
export type {GridCell, ClusterCell, Cluster} from './managers'
|
|
66
82
|
export type {
|
|
67
83
|
CharterProgress,
|
|
@@ -125,7 +141,13 @@ export {
|
|
|
125
141
|
categoryLabelFromIndex,
|
|
126
142
|
} from './data/catalog'
|
|
127
143
|
export {getItemFamily, itemFamilies, itemFamilyKey} from './data/metadata'
|
|
128
|
-
export type {ItemFamily} from './data/metadata'
|
|
144
|
+
export type {ComponentProcess, ItemFamily} from './data/metadata'
|
|
145
|
+
export {
|
|
146
|
+
COMPONENT_PROCESS_ORDER,
|
|
147
|
+
completeComponentSet,
|
|
148
|
+
getComponentProcess,
|
|
149
|
+
} from './data/component-process'
|
|
150
|
+
export type {CompleteComponentSet} from './data/component-process'
|
|
129
151
|
export {getCurrentEpoch, getEpochInfo} from './scheduling/epoch'
|
|
130
152
|
export type {EpochInfo} from './scheduling/epoch'
|
|
131
153
|
export {
|
|
@@ -341,6 +363,7 @@ export {
|
|
|
341
363
|
jobDropoffTask,
|
|
342
364
|
jobCancelRoute,
|
|
343
365
|
buildJobCancelRoute,
|
|
366
|
+
hostedDropoff,
|
|
344
367
|
jobCancellable,
|
|
345
368
|
jobCancellationBlockReason,
|
|
346
369
|
jobDeposited,
|
|
@@ -356,7 +379,10 @@ export type {
|
|
|
356
379
|
JobStatusInput,
|
|
357
380
|
JobCancelRoute,
|
|
358
381
|
JobCancellationBlockReason,
|
|
382
|
+
JobRouteOptions,
|
|
383
|
+
HostedLeg,
|
|
359
384
|
OwnedJob,
|
|
385
|
+
BuildJob,
|
|
360
386
|
PickupInFlight,
|
|
361
387
|
} from './scheduling/jobs'
|
|
362
388
|
|
|
@@ -400,6 +426,7 @@ export {
|
|
|
400
426
|
cargoInputKey,
|
|
401
427
|
hasSourceCoupling,
|
|
402
428
|
hasIncomingCoupling,
|
|
429
|
+
isHostedCivicLeg,
|
|
403
430
|
} from './scheduling/availability'
|
|
404
431
|
export type {CargoInput, IncomingHoldEntity, IncomingSource} from './scheduling/availability'
|
|
405
432
|
|
|
@@ -409,7 +436,7 @@ export {calcClusterIntake, calcClustercraftDuration, INTAKE_RATE} from './capabi
|
|
|
409
436
|
|
|
410
437
|
export {maxCraftable} from './capabilities/craftable'
|
|
411
438
|
|
|
412
|
-
export {energyAtTime} from './scheduling/energy'
|
|
439
|
+
export {energyAtTime, energyDrawsFunded} from './scheduling/energy'
|
|
413
440
|
|
|
414
441
|
export * from './types/capabilities'
|
|
415
442
|
export * from './types/entity'
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
pendingCivicTransfers,
|
|
13
13
|
selectCivicShuttleBay,
|
|
14
14
|
CIVIC_TRANSFER_PER_PLAYER_CAP,
|
|
15
|
+
type CivicShuttleBay,
|
|
15
16
|
} from './civic-shuttle'
|
|
16
17
|
|
|
17
18
|
type Lane = ServerContract.Types.lane
|
|
@@ -63,6 +64,32 @@ function shuttleTask(senderId: number, receiverId: number, group?: number): Task
|
|
|
63
64
|
} as unknown as Task
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
function civicTask(type: number, couplings: unknown[]): Task {
|
|
68
|
+
return {
|
|
69
|
+
type: UInt8.from(type),
|
|
70
|
+
duration: UInt32.from(60),
|
|
71
|
+
cancelable: UInt8.from(2),
|
|
72
|
+
cargo: [],
|
|
73
|
+
couplings,
|
|
74
|
+
} as unknown as Task
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function pullCoupling(id: number) {
|
|
78
|
+
return {
|
|
79
|
+
counterpart: {entity_id: UInt64.from(id), entity_type: UInt8.from(1)},
|
|
80
|
+
hold: UInt64.from(1),
|
|
81
|
+
kind: UInt8.from(HoldKind.PULL),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function pushCoupling(id: number) {
|
|
86
|
+
return {
|
|
87
|
+
counterpart: {entity_id: UInt64.from(id), entity_type: UInt8.from(1)},
|
|
88
|
+
hold: UInt64.from(1),
|
|
89
|
+
kind: UInt8.from(HoldKind.PUSH),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
66
93
|
function makeLane(laneKey: number, tasks: Task[]): Lane {
|
|
67
94
|
return {
|
|
68
95
|
lane_key: UInt8.from(laneKey),
|
|
@@ -142,6 +169,13 @@ test('selectCivicShuttleBay takes the first free bay, then the lowest occupied o
|
|
|
142
169
|
expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, bothBusy, T0)?.laneKey).toBe(1)
|
|
143
170
|
})
|
|
144
171
|
|
|
172
|
+
test('selectCivicShuttleBay picks the earliest finish, not the first free bay', () => {
|
|
173
|
+
const modules = depotModules()
|
|
174
|
+
const lanes = [makeLane(1, [shuttleTask(20, 21)])]
|
|
175
|
+
const fast = (bay: CivicShuttleBay) => (bay.slotIndex === 0 ? 10 : 500)
|
|
176
|
+
expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, lanes, T0, fast)?.slotIndex).toBe(0)
|
|
177
|
+
})
|
|
178
|
+
|
|
145
179
|
test('selectCivicShuttleBay returns nothing when the building has no loaders', () => {
|
|
146
180
|
const modules = [makeModuleEntry(ITEM_STORAGE_T1, storageStats)]
|
|
147
181
|
expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, [], T0)).toBeUndefined()
|
|
@@ -190,6 +224,30 @@ test('civicTransfersFrom counts only the ships a player sends from', () => {
|
|
|
190
224
|
expect(civicTransfersFrom(entity, [99], T0).length).toBe(0)
|
|
191
225
|
})
|
|
192
226
|
|
|
227
|
+
test('civic legs on bays count toward the player cap', () => {
|
|
228
|
+
const depot = {
|
|
229
|
+
lanes: [makeLane(1, [civicTask(TaskType.CIVIC_WITHDRAW, [pushCoupling(7)])])],
|
|
230
|
+
} as never
|
|
231
|
+
expect(civicTransfersFrom(depot, [7]).length).toBe(1)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('a civic deposit leg reports the same player id as sender and receiver', () => {
|
|
235
|
+
const depot = {
|
|
236
|
+
lanes: [makeLane(1, [civicTask(TaskType.CIVIC_DEPOSIT, [pullCoupling(7)])])],
|
|
237
|
+
} as never
|
|
238
|
+
const [transfer] = pendingCivicTransfers(depot, T0)
|
|
239
|
+
expect(String(transfer.senderId)).toBe('7')
|
|
240
|
+
expect(String(transfer.receiverId)).toBe('7')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('a civic pickup leg is not callable while CANCEL_NEVER', () => {
|
|
244
|
+
const task = civicTask(TaskType.CIVIC_WITHDRAW, [pushCoupling(7)])
|
|
245
|
+
;(task as unknown as {cancelable: unknown}).cancelable = UInt8.from(0)
|
|
246
|
+
const depot = {lanes: [makeLane(1, [task])]} as never
|
|
247
|
+
const [transfer] = pendingCivicTransfers(depot, T0)
|
|
248
|
+
expect(transfer.callable).toBe(false)
|
|
249
|
+
})
|
|
250
|
+
|
|
193
251
|
test('civicTransfersAtCap trips at the per-player cap', () => {
|
|
194
252
|
const mine = Array.from({length: CIVIC_TRANSFER_PER_PLAYER_CAP}, (_, i) =>
|
|
195
253
|
shuttleTask(20 + i, 50)
|
|
@@ -4,7 +4,7 @@ import {getItem} from '../data/catalog'
|
|
|
4
4
|
import {JOB_QUEUE_CAP} from '../scheduling/jobs'
|
|
5
5
|
import {laneKeyForModule, resolveLaneLoader} from '../scheduling/lanes'
|
|
6
6
|
import {orderedTasks, type ScheduleData} from '../scheduling/schedule'
|
|
7
|
-
import {HoldKind, TaskType} from '../types'
|
|
7
|
+
import {HoldKind, TaskCancelable, TaskType} from '../types'
|
|
8
8
|
|
|
9
9
|
type CargoItem = ServerContract.Types.cargo_item
|
|
10
10
|
type Lane = ServerContract.Types.lane
|
|
@@ -22,7 +22,6 @@ export interface CivicShuttleBay {
|
|
|
22
22
|
full: boolean
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// a landed booking clears during the next civicshuttle, so only unfinished tasks occupy a bay
|
|
26
25
|
function unfinishedTasks(lane: Lane | undefined, now: Date): number {
|
|
27
26
|
if (!lane) return 0
|
|
28
27
|
const startedMs = lane.schedule.started.toDate().getTime()
|
|
@@ -64,15 +63,38 @@ export function civicShuttleBays(
|
|
|
64
63
|
return bays
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
|
|
66
|
+
function laneEndMs(lane: Lane | undefined, nowMs: number): number {
|
|
67
|
+
if (!lane) return nowMs
|
|
68
|
+
let endSec = 0
|
|
69
|
+
for (const task of lane.schedule.tasks) endSec += task.duration.toNumber()
|
|
70
|
+
return Math.max(nowMs, lane.schedule.started.toDate().getTime() + endSec * 1000)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Mirrors cargo.cpp pick_civic_bay: earliest finish among bays below the queue cap.
|
|
68
74
|
export function selectCivicShuttleBay(
|
|
69
75
|
modules: ModuleEntry[],
|
|
70
76
|
entityItemId: number,
|
|
71
77
|
lanes: Lane[],
|
|
72
|
-
now: Date = new Date()
|
|
78
|
+
now: Date = new Date(),
|
|
79
|
+
durationFor: (bay: CivicShuttleBay) => number = () => 0,
|
|
80
|
+
minStart?: Date
|
|
73
81
|
): CivicShuttleBay | undefined {
|
|
74
82
|
const bays = civicShuttleBays(modules, entityItemId, lanes, now)
|
|
75
|
-
|
|
83
|
+
const nowMs = now.getTime()
|
|
84
|
+
const floorMs = Math.max(nowMs, minStart?.getTime() ?? nowMs)
|
|
85
|
+
let best: CivicShuttleBay | undefined
|
|
86
|
+
let bestEnd = Infinity
|
|
87
|
+
for (const bay of bays) {
|
|
88
|
+
if (bay.full) continue
|
|
89
|
+
const lane = lanes.find((l) => l.lane_key.toNumber() === bay.laneKey)
|
|
90
|
+
const start = Math.max(laneEndMs(lane, nowMs), floorMs)
|
|
91
|
+
const end = start + durationFor(bay) * 1000
|
|
92
|
+
if (end < bestEnd) {
|
|
93
|
+
best = bay
|
|
94
|
+
bestEnd = end
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return best ?? bays[0]
|
|
76
98
|
}
|
|
77
99
|
|
|
78
100
|
export interface PendingCivicTransfer {
|
|
@@ -98,7 +120,31 @@ export function pendingCivicTransfers(
|
|
|
98
120
|
}
|
|
99
121
|
const out: PendingCivicTransfer[] = []
|
|
100
122
|
for (const entry of orderedTasks(entity)) {
|
|
101
|
-
|
|
123
|
+
const type = entry.task.type.toNumber()
|
|
124
|
+
if (type === TaskType.CIVIC_DEPOSIT || type === TaskType.CIVIC_WITHDRAW) {
|
|
125
|
+
const side = entry.task.couplings[0]
|
|
126
|
+
if (!side) continue
|
|
127
|
+
const isTail = tailIndex.get(entry.laneKey) === entry.taskIndex
|
|
128
|
+
const complete = entry.completesAt.getTime() <= now.getTime()
|
|
129
|
+
out.push({
|
|
130
|
+
laneKey: entry.laneKey,
|
|
131
|
+
taskIndex: entry.taskIndex,
|
|
132
|
+
senderId: side.counterpart.entity_id,
|
|
133
|
+
receiverId: side.counterpart.entity_id,
|
|
134
|
+
cargo: entry.task.cargo,
|
|
135
|
+
startsAt: entry.startsAt,
|
|
136
|
+
completesAt: entry.completesAt,
|
|
137
|
+
isTail,
|
|
138
|
+
complete,
|
|
139
|
+
callable:
|
|
140
|
+
isTail &&
|
|
141
|
+
!complete &&
|
|
142
|
+
!entry.task.entitygroup &&
|
|
143
|
+
entry.task.cancelable.toNumber() !== TaskCancelable.NEVER,
|
|
144
|
+
})
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
if (type !== TaskType.SHUTTLE) continue
|
|
102
148
|
const pull = entry.task.couplings.find((c) => c.kind.toNumber() === HoldKind.PULL)
|
|
103
149
|
const push = entry.task.couplings.find((c) => c.kind.toNumber() === HoldKind.PUSH)
|
|
104
150
|
if (!pull || !push) continue
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import type {NameType} from '@wharfkit/antelope'
|
|
2
|
-
import type {ServerContract} from '../contracts'
|
|
3
|
-
import {getEntityClass} from '../data/kind-registry'
|
|
4
1
|
import {computeLoaderMass, computeLoaderThrust} from '../nft/description'
|
|
5
|
-
import {laneKeyForModule, resolveLaneLoader} from '../scheduling/lanes'
|
|
6
|
-
import {calc_onesided_duration} from '../travel/travel'
|
|
7
2
|
import {MASS_STAT_SCALE, MASS_UNITS_PER_TONNE, PRECISION} from '../types'
|
|
8
3
|
import {CIVIC_LOADER_STAT, CIVIC_LOADER_TIER, DEFAULT_ORBITAL_Z} from './constants'
|
|
9
4
|
|
|
@@ -39,63 +34,3 @@ export function contributeDuration(totalMass: number, altitudeZ = 0): number {
|
|
|
39
34
|
export function contributeDurationForTonnes(tonnes: number, altitudeZ = 0): number {
|
|
40
35
|
return contributeDuration(Math.floor(tonnes * MASS_UNITS_PER_TONNE), altitudeZ)
|
|
41
36
|
}
|
|
42
|
-
|
|
43
|
-
export const DEPOT_LOADER_SLOT = 0
|
|
44
|
-
|
|
45
|
-
export interface DepotTransferParams {
|
|
46
|
-
depotModules: ServerContract.Types.module_entry[]
|
|
47
|
-
depotItemId: number
|
|
48
|
-
depotKind: NameType
|
|
49
|
-
depotZ: number
|
|
50
|
-
shipKind: NameType
|
|
51
|
-
shipZ: number
|
|
52
|
-
cargoMass: number
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Mirrors depot.cpp: the depot's slot-0 loader drives the transfer; 0 when no loader is installed.
|
|
56
|
-
export function depotTransferDuration(params: DepotTransferParams): number {
|
|
57
|
-
const loader = resolveLaneLoader(
|
|
58
|
-
params.depotModules,
|
|
59
|
-
params.depotItemId,
|
|
60
|
-
laneKeyForModule(DEPOT_LOADER_SLOT)
|
|
61
|
-
)
|
|
62
|
-
if (!loader.valid) return 0
|
|
63
|
-
return calc_onesided_duration(
|
|
64
|
-
loader.thrust,
|
|
65
|
-
loader.mass,
|
|
66
|
-
params.shipZ,
|
|
67
|
-
params.depotZ,
|
|
68
|
-
getEntityClass(params.shipKind),
|
|
69
|
-
getEntityClass(params.depotKind),
|
|
70
|
-
params.cargoMass
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface CivicDropoffParams {
|
|
75
|
-
buildingModules: ServerContract.Types.module_entry[]
|
|
76
|
-
buildingItemId: number
|
|
77
|
-
buildingKind: NameType
|
|
78
|
-
buildingZ: number
|
|
79
|
-
shipKind: NameType
|
|
80
|
-
shipZ: number
|
|
81
|
-
cargoMass: number
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Mirrors jobs.cpp craftjob: the building's slot-0 loader when one is installed, the civic loader when none is.
|
|
85
|
-
export function civicDropoffDuration(params: CivicDropoffParams): number {
|
|
86
|
-
const loader = resolveLaneLoader(
|
|
87
|
-
params.buildingModules,
|
|
88
|
-
params.buildingItemId,
|
|
89
|
-
laneKeyForModule(DEPOT_LOADER_SLOT)
|
|
90
|
-
)
|
|
91
|
-
if (!loader.valid) return contributeDuration(params.cargoMass, params.shipZ)
|
|
92
|
-
return calc_onesided_duration(
|
|
93
|
-
loader.thrust,
|
|
94
|
-
loader.mass,
|
|
95
|
-
params.shipZ,
|
|
96
|
-
params.buildingZ,
|
|
97
|
-
getEntityClass(params.shipKind),
|
|
98
|
-
getEntityClass(params.buildingKind),
|
|
99
|
-
params.cargoMass
|
|
100
|
-
)
|
|
101
|
-
}
|
package/src/influence/index.ts
CHANGED
|
@@ -26,24 +26,23 @@ export {
|
|
|
26
26
|
export {
|
|
27
27
|
componentBaseAtomic,
|
|
28
28
|
componentEffortAtomic,
|
|
29
|
+
explainCargoItem,
|
|
29
30
|
pricingFromWeights,
|
|
30
31
|
resourceValueAtomic,
|
|
31
32
|
valueCargoItem,
|
|
32
33
|
valueContribution,
|
|
33
34
|
weightedQualityAtomic,
|
|
35
|
+
type CargoValuation,
|
|
36
|
+
type ComponentValuation,
|
|
34
37
|
type InfluencePricing,
|
|
38
|
+
type ResourceValuation,
|
|
35
39
|
type ValuedItem,
|
|
36
40
|
} from './valuation'
|
|
37
41
|
export {
|
|
38
|
-
civicDropoffDuration,
|
|
39
42
|
civicLoader,
|
|
40
43
|
contributeDuration,
|
|
41
44
|
contributeDurationForTonnes,
|
|
42
|
-
depotTransferDuration,
|
|
43
|
-
DEPOT_LOADER_SLOT,
|
|
44
|
-
type CivicDropoffParams,
|
|
45
45
|
type CivicLoaderStats,
|
|
46
|
-
type DepotTransferParams,
|
|
47
46
|
} from './duration'
|
|
48
47
|
export {
|
|
49
48
|
civicShuttleBays,
|