@shipload/sdk 1.0.0-next.73 → 1.0.0-next.74
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 +502 -127
- package/lib/shipload.js +5871 -4906
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +5858 -4900
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +174 -47
- package/lib/testing.js +524 -142
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +524 -142
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/contracts/server.ts +457 -134
- package/src/data/entities.json +2 -2
- package/src/index-module.ts +22 -1
- 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/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
package/src/data/entities.json
CHANGED
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,
|
|
@@ -341,6 +357,7 @@ export {
|
|
|
341
357
|
jobDropoffTask,
|
|
342
358
|
jobCancelRoute,
|
|
343
359
|
buildJobCancelRoute,
|
|
360
|
+
hostedDropoff,
|
|
344
361
|
jobCancellable,
|
|
345
362
|
jobCancellationBlockReason,
|
|
346
363
|
jobDeposited,
|
|
@@ -356,7 +373,10 @@ export type {
|
|
|
356
373
|
JobStatusInput,
|
|
357
374
|
JobCancelRoute,
|
|
358
375
|
JobCancellationBlockReason,
|
|
376
|
+
JobRouteOptions,
|
|
377
|
+
HostedLeg,
|
|
359
378
|
OwnedJob,
|
|
379
|
+
BuildJob,
|
|
360
380
|
PickupInFlight,
|
|
361
381
|
} from './scheduling/jobs'
|
|
362
382
|
|
|
@@ -400,6 +420,7 @@ export {
|
|
|
400
420
|
cargoInputKey,
|
|
401
421
|
hasSourceCoupling,
|
|
402
422
|
hasIncomingCoupling,
|
|
423
|
+
isHostedCivicLeg,
|
|
403
424
|
} from './scheduling/availability'
|
|
404
425
|
export type {CargoInput, IncomingHoldEntity, IncomingSource} from './scheduling/availability'
|
|
405
426
|
|
|
@@ -409,7 +430,7 @@ export {calcClusterIntake, calcClustercraftDuration, INTAKE_RATE} from './capabi
|
|
|
409
430
|
|
|
410
431
|
export {maxCraftable} from './capabilities/craftable'
|
|
411
432
|
|
|
412
|
-
export {energyAtTime} from './scheduling/energy'
|
|
433
|
+
export {energyAtTime, energyDrawsFunded} from './scheduling/energy'
|
|
413
434
|
|
|
414
435
|
export * from './types/capabilities'
|
|
415
436
|
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,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {describe, expect, test} from 'bun:test'
|
|
2
|
-
import {Checksum256
|
|
3
|
-
import type {ServerContract} from '../contracts'
|
|
2
|
+
import {Checksum256} from '@wharfkit/antelope'
|
|
4
3
|
import {ITEM_ORE_T1, ITEM_PLATE, ITEM_CRYSTAL_T1} from '../data/item-ids'
|
|
5
4
|
import {categoryIndex} from './categories'
|
|
6
5
|
import {
|
|
@@ -20,9 +19,10 @@ import {
|
|
|
20
19
|
popcount5,
|
|
21
20
|
} from './demand'
|
|
22
21
|
import {findDecomp, DECOMP_REGISTRY} from './decomp'
|
|
23
|
-
import {
|
|
22
|
+
import {contributeDuration} from './duration'
|
|
24
23
|
import {getStatCount, statsSumSq} from './quality'
|
|
25
|
-
import {pricingFromWeights, valueCargoItem, valueContribution} from './valuation'
|
|
24
|
+
import {explainCargoItem, pricingFromWeights, valueCargoItem, valueContribution} from './valuation'
|
|
25
|
+
import {getItems} from '../data/catalog'
|
|
26
26
|
import {citizenryName, citizenryPatternCount} from './citizenry'
|
|
27
27
|
import citizenryAdjectives from '../data/citizenry-adjectives.json'
|
|
28
28
|
import citizenryNouns from '../data/citizenry-nouns.json'
|
|
@@ -155,6 +155,53 @@ describe('decomposition', () => {
|
|
|
155
155
|
})
|
|
156
156
|
})
|
|
157
157
|
|
|
158
|
+
describe('explainCargoItem', () => {
|
|
159
|
+
test('factors reproduce valueCargoItem for every valuable catalog item', () => {
|
|
160
|
+
const acute = buildDemand(0, categoryIndex('regolith'), DEMAND_TRIPLE_SEED)
|
|
161
|
+
for (const def of getItems()) {
|
|
162
|
+
if (def.type !== 'resource' && def.type !== 'component') continue
|
|
163
|
+
const item = {itemId: Number(def.id), quantity: 7, stats: packStats(363, 535, 246)}
|
|
164
|
+
const explained = explainCargoItem(item, acute, pricing)
|
|
165
|
+
expect(explained.totalAtomic).toBe(valueCargoItem(item, acute, pricing))
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('a resource explains mass, weight, quality and need', () => {
|
|
170
|
+
const acute = buildDemand(0, categoryIndex('crystal'), DEMAND_TRIPLE_SEED)
|
|
171
|
+
const explained = explainCargoItem(
|
|
172
|
+
{itemId: ITEM_CRYSTAL_T1, quantity: 5000, stats: packStats(PAR)},
|
|
173
|
+
acute,
|
|
174
|
+
pricing
|
|
175
|
+
)
|
|
176
|
+
if (explained.kind !== 'resource') throw new Error('expected resource')
|
|
177
|
+
expect(explained.mass).toBe(
|
|
178
|
+
BigInt(getItems().find((i) => Number(i.id) === ITEM_CRYSTAL_T1)!.mass) * 5000n
|
|
179
|
+
)
|
|
180
|
+
expect(explained.weight).toBe(1)
|
|
181
|
+
expect(explained.quality).toBeCloseTo(1, 6)
|
|
182
|
+
expect(explained.need).toBe(2)
|
|
183
|
+
expect(explained.points).toBeCloseTo(
|
|
184
|
+
(Number(explained.mass) / 10) * explained.weight * explained.quality * explained.need,
|
|
185
|
+
3
|
|
186
|
+
)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('a component explains raw mass, material weight, quality and effort', () => {
|
|
190
|
+
const explained = explainCargoItem(
|
|
191
|
+
{itemId: ITEM_PLATE, quantity: 3, stats: packStats(PAR)},
|
|
192
|
+
baseDemand,
|
|
193
|
+
pricing
|
|
194
|
+
)
|
|
195
|
+
if (explained.kind !== 'component') throw new Error('expected component')
|
|
196
|
+
const entry = findDecomp(ITEM_PLATE)!
|
|
197
|
+
const raw = entry.buckets.reduce((sum, b) => sum + b.rawMass, 0)
|
|
198
|
+
expect(explained.rawMass).toBe(raw * 3)
|
|
199
|
+
expect(explained.processedMass).toBe(entry.processedMass * 3)
|
|
200
|
+
expect(explained.weight).toBeGreaterThanOrEqual(1)
|
|
201
|
+
expect(explained.baseAtomic + explained.effortAtomic).toBe(explained.totalAtomic)
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
158
205
|
describe('valuation', () => {
|
|
159
206
|
test('a tonne of par T1 ore at base demand is worth exactly one point', () => {
|
|
160
207
|
const value = valueCargoItem(
|
|
@@ -316,135 +363,3 @@ describe('scales', () => {
|
|
|
316
363
|
expect(NEED_FP_SCALE % INFLUENCE_ATOMIC_PER_POINT).toBe(0)
|
|
317
364
|
})
|
|
318
365
|
})
|
|
319
|
-
|
|
320
|
-
describe('depot transfer duration', () => {
|
|
321
|
-
type ModuleEntry = ServerContract.Types.module_entry
|
|
322
|
-
const DEPOT_ITEM = 10219
|
|
323
|
-
const LOADER_T1 = 10103
|
|
324
|
-
|
|
325
|
-
function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
|
|
326
|
-
return {
|
|
327
|
-
type: UInt8.from(0),
|
|
328
|
-
installed: {item_id: UInt16.from(itemId), stats: UInt64.from(stats)},
|
|
329
|
-
} as unknown as ModuleEntry
|
|
330
|
-
}
|
|
331
|
-
function emptySlot(): ModuleEntry {
|
|
332
|
-
return {type: UInt8.from(0)} as unknown as ModuleEntry
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// A spawned depot: loader in slot 0, four empty bays after it.
|
|
336
|
-
const depotWithLoader = (): ModuleEntry[] => [
|
|
337
|
-
makeModuleEntry(LOADER_T1, packStats(500)),
|
|
338
|
-
emptySlot(),
|
|
339
|
-
emptySlot(),
|
|
340
|
-
emptySlot(),
|
|
341
|
-
emptySlot(),
|
|
342
|
-
]
|
|
343
|
-
const params = (overrides: Record<string, unknown> = {}) => ({
|
|
344
|
-
depotModules: depotWithLoader(),
|
|
345
|
-
depotItemId: DEPOT_ITEM,
|
|
346
|
-
depotKind: 'depot',
|
|
347
|
-
depotZ: 0,
|
|
348
|
-
shipKind: 'ship',
|
|
349
|
-
shipZ: 0,
|
|
350
|
-
cargoMass: 100,
|
|
351
|
-
...overrides,
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
test('drives off the depot loader, not the ship', () => {
|
|
355
|
-
expect(depotTransferDuration(params())).toBeGreaterThan(0)
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
test('grows with cargo mass', () => {
|
|
359
|
-
expect(depotTransferDuration(params({cargoMass: 10_000}))).toBeGreaterThan(
|
|
360
|
-
depotTransferDuration(params({cargoMass: 100}))
|
|
361
|
-
)
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
test('empty cargo costs nothing', () => {
|
|
365
|
-
expect(depotTransferDuration(params({cargoMass: 0}))).toBe(0)
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
test('a depot with no loader installed cannot transfer', () => {
|
|
369
|
-
expect(depotTransferDuration(params({depotModules: [emptySlot()]}))).toBe(0)
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
test('co-located transfers are floored at the orbital minimum distance', () => {
|
|
373
|
-
// max(planetary 100, orbital 200) = 200, so anything under 200 reads as 200
|
|
374
|
-
const touching = depotTransferDuration(params({shipZ: 0, depotZ: 0}))
|
|
375
|
-
const under = depotTransferDuration(params({shipZ: 150, depotZ: 0}))
|
|
376
|
-
const over = depotTransferDuration(params({shipZ: 400, depotZ: 0}))
|
|
377
|
-
expect(under).toBe(touching)
|
|
378
|
-
expect(over).toBeGreaterThan(touching)
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
test('never reports zero for real cargo', () => {
|
|
382
|
-
expect(depotTransferDuration(params({cargoMass: 1}))).toBeGreaterThanOrEqual(1)
|
|
383
|
-
})
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
describe('civic drop-off duration', () => {
|
|
387
|
-
type ModuleEntry = ServerContract.Types.module_entry
|
|
388
|
-
const DEPOT_ITEM = 10219
|
|
389
|
-
const WORKSHOP_ITEM = 10208
|
|
390
|
-
const LOADER_T1 = 10103
|
|
391
|
-
|
|
392
|
-
function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
|
|
393
|
-
return {
|
|
394
|
-
type: UInt8.from(0),
|
|
395
|
-
installed: {item_id: UInt16.from(itemId), stats: UInt64.from(stats)},
|
|
396
|
-
} as unknown as ModuleEntry
|
|
397
|
-
}
|
|
398
|
-
function emptySlot(): ModuleEntry {
|
|
399
|
-
return {type: UInt8.from(0)} as unknown as ModuleEntry
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const withLoader = () => ({
|
|
403
|
-
buildingModules: [makeModuleEntry(LOADER_T1, packStats(500)), emptySlot()],
|
|
404
|
-
buildingItemId: DEPOT_ITEM,
|
|
405
|
-
buildingKind: 'depot',
|
|
406
|
-
buildingZ: 0,
|
|
407
|
-
shipKind: 'ship',
|
|
408
|
-
shipZ: 400,
|
|
409
|
-
cargoMass: 100,
|
|
410
|
-
})
|
|
411
|
-
const withoutLoader = () => ({
|
|
412
|
-
buildingModules: [] as ModuleEntry[],
|
|
413
|
-
buildingItemId: WORKSHOP_ITEM,
|
|
414
|
-
buildingKind: 'workshop',
|
|
415
|
-
buildingZ: 0,
|
|
416
|
-
shipKind: 'ship',
|
|
417
|
-
shipZ: 400,
|
|
418
|
-
cargoMass: 100,
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
test('a building with a slot-0 loader prices off that loader', () => {
|
|
422
|
-
const p = withLoader()
|
|
423
|
-
expect(civicDropoffDuration(p)).toBe(
|
|
424
|
-
depotTransferDuration({
|
|
425
|
-
depotModules: p.buildingModules,
|
|
426
|
-
depotItemId: p.buildingItemId,
|
|
427
|
-
depotKind: p.buildingKind,
|
|
428
|
-
depotZ: p.buildingZ,
|
|
429
|
-
shipKind: p.shipKind,
|
|
430
|
-
shipZ: p.shipZ,
|
|
431
|
-
cargoMass: p.cargoMass,
|
|
432
|
-
})
|
|
433
|
-
)
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
test('a building with no loader falls back to the civic loader', () => {
|
|
437
|
-
const p = withoutLoader()
|
|
438
|
-
expect(civicDropoffDuration(p)).toBe(contributeDuration(p.cargoMass, p.shipZ))
|
|
439
|
-
expect(civicDropoffDuration(p)).toBeGreaterThan(0)
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
test('grows with cargo mass on both paths', () => {
|
|
443
|
-
expect(civicDropoffDuration({...withLoader(), cargoMass: 10_000})).toBeGreaterThan(
|
|
444
|
-
civicDropoffDuration(withLoader())
|
|
445
|
-
)
|
|
446
|
-
expect(civicDropoffDuration({...withoutLoader(), cargoMass: 10_000})).toBeGreaterThan(
|
|
447
|
-
civicDropoffDuration(withoutLoader())
|
|
448
|
-
)
|
|
449
|
-
})
|
|
450
|
-
})
|
|
@@ -126,6 +126,81 @@ export function valueCargoItem(
|
|
|
126
126
|
return withEffort
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
export interface ResourceValuation {
|
|
130
|
+
kind: 'resource'
|
|
131
|
+
category: number
|
|
132
|
+
tier: number
|
|
133
|
+
mass: bigint
|
|
134
|
+
weight: number
|
|
135
|
+
quality: number
|
|
136
|
+
need: number
|
|
137
|
+
totalAtomic: bigint
|
|
138
|
+
points: number
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ComponentValuation {
|
|
142
|
+
kind: 'component'
|
|
143
|
+
rawMass: number
|
|
144
|
+
processedMass: number
|
|
145
|
+
weight: number
|
|
146
|
+
quality: number
|
|
147
|
+
baseAtomic: bigint
|
|
148
|
+
effortAtomic: bigint
|
|
149
|
+
totalAtomic: bigint
|
|
150
|
+
points: number
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type CargoValuation = ResourceValuation | ComponentValuation
|
|
154
|
+
|
|
155
|
+
export function explainCargoItem(
|
|
156
|
+
item: ValuedItem,
|
|
157
|
+
demand: DemandView,
|
|
158
|
+
pricing: InfluencePricing
|
|
159
|
+
): CargoValuation {
|
|
160
|
+
const def = getItem(item.itemId)
|
|
161
|
+
const totalAtomic = valueCargoItem(item, demand, pricing)
|
|
162
|
+
const points = Number(totalAtomic) / INFLUENCE_ATOMIC_PER_POINT
|
|
163
|
+
const nStats = getStatCount(item.itemId)
|
|
164
|
+
const quality = statsSumSq(item.stats, nStats) / (nStats * pricing.d1)
|
|
165
|
+
|
|
166
|
+
if (def.type === 'resource') {
|
|
167
|
+
if (def.category === undefined) throw new Error('resource has no category')
|
|
168
|
+
const category = categoryIndex(def.category)
|
|
169
|
+
return {
|
|
170
|
+
kind: 'resource',
|
|
171
|
+
category,
|
|
172
|
+
tier: def.tier,
|
|
173
|
+
mass: BigInt(def.mass) * BigInt(item.quantity),
|
|
174
|
+
weight: Number(pricing.weight(category, def.tier)) / WEIGHT_FP_SCALE,
|
|
175
|
+
quality,
|
|
176
|
+
need: Number(needForCategory(demand, category)) / NEED_FP_SCALE,
|
|
177
|
+
totalAtomic,
|
|
178
|
+
points,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const entry = findDecomp(item.itemId)
|
|
183
|
+
if (!entry) throw new Error('item has no decomposition')
|
|
184
|
+
let rawMass = 0
|
|
185
|
+
let weighted = 0n
|
|
186
|
+
for (const bucket of entry.buckets) {
|
|
187
|
+
rawMass += bucket.rawMass
|
|
188
|
+
weighted += BigInt(bucket.rawMass) * pricing.weight(bucket.category, bucket.tier)
|
|
189
|
+
}
|
|
190
|
+
const effortAtomic = componentEffortAtomic(entry.processedMass, item.quantity)
|
|
191
|
+
return {
|
|
192
|
+
kind: 'component',
|
|
193
|
+
rawMass: rawMass * item.quantity,
|
|
194
|
+
processedMass: entry.processedMass * item.quantity,
|
|
195
|
+
weight: rawMass === 0 ? 0 : Number(weighted) / (rawMass * WEIGHT_FP_SCALE),
|
|
196
|
+
quality,
|
|
197
|
+
baseAtomic: totalAtomic - effortAtomic,
|
|
198
|
+
effortAtomic,
|
|
199
|
+
totalAtomic,
|
|
200
|
+
points,
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
129
204
|
export function valueContribution(
|
|
130
205
|
bundle: ValuedItem[],
|
|
131
206
|
demand: DemandView,
|