@shipload/sdk 1.0.0-next.42 → 1.0.0-next.44
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 +222 -172
- package/lib/shipload.js +658 -421
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +644 -423
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +50 -67
- package/lib/testing.js +168 -208
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +169 -209
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/contracts/server.ts +126 -214
- package/src/data/capabilities.ts +22 -14
- package/src/data/capability-formulas.ts +7 -7
- package/src/data/entities.json +4 -0
- package/src/data/item-ids.ts +1 -0
- package/src/data/items.json +6 -0
- package/src/data/kind-registry.json +18 -6
- package/src/data/kind-registry.ts +7 -0
- package/src/data/metadata.ts +22 -15
- package/src/data/recipes.json +147 -138
- package/src/derivation/build-methods.test.ts +13 -0
- package/src/derivation/build-methods.ts +5 -2
- package/src/derivation/capabilities.ts +8 -8
- package/src/derivation/capability-mappings.ts +49 -4
- package/src/index-module.ts +21 -1
- package/src/managers/actions.ts +42 -8
- package/src/managers/cluster.test.ts +39 -0
- package/src/managers/cluster.ts +53 -0
- package/src/managers/context.ts +9 -0
- package/src/managers/index.ts +2 -0
- package/src/nft/buildImmutableData.ts +15 -17
- package/src/nft/description.ts +8 -8
- package/src/resolution/describe-module.ts +6 -6
- package/src/resolution/resolve-item.ts +3 -3
- package/src/scheduling/unwrap.test.ts +60 -0
- package/src/scheduling/unwrap.ts +187 -0
- package/src/shipload.ts +5 -0
- package/src/subscriptions/manager.cluster.test.ts +46 -0
- package/src/subscriptions/manager.ts +22 -0
- package/src/subscriptions/types.ts +16 -0
- package/src/types.ts +7 -0
package/src/managers/actions.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type UInt64Type,
|
|
18
18
|
} from '@wharfkit/antelope'
|
|
19
19
|
import {BaseManager} from './base'
|
|
20
|
-
import {Coordinates, PRECISION, type CoordinatesType} from '../types'
|
|
20
|
+
import {Coordinates, PRECISION, type ClusterSlotType, type CoordinatesType} from '../types'
|
|
21
21
|
import {ServerContract} from '../contracts'
|
|
22
22
|
import {ATOMICASSETS_ABI, SHIPLOAD_COLLECTION} from '../nft/atomicassets'
|
|
23
23
|
import {getItem} from '../data/catalog'
|
|
@@ -435,22 +435,39 @@ export class ActionsManager extends BaseManager {
|
|
|
435
435
|
})
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
-
deploy(
|
|
438
|
+
deploy(
|
|
439
|
+
entityId: UInt64Type,
|
|
440
|
+
ref: ServerContract.ActionParams.Type.cargo_ref,
|
|
441
|
+
slot?: ClusterSlotType
|
|
442
|
+
): Action {
|
|
439
443
|
return this.server.action('deploy', {
|
|
440
444
|
id: UInt64.from(entityId),
|
|
441
445
|
ref,
|
|
446
|
+
slot: slot ? {hub: UInt64.from(slot.hub), gx: slot.gx, gy: slot.gy} : undefined,
|
|
442
447
|
})
|
|
443
448
|
}
|
|
444
449
|
|
|
445
|
-
claimplot(
|
|
446
|
-
entityId: UInt64Type,
|
|
447
|
-
targetItemId: UInt16Type,
|
|
448
|
-
coords: ServerContract.ActionParams.Type.coordinates
|
|
449
|
-
): Action {
|
|
450
|
+
claimplot(entityId: UInt64Type, targetItemId: UInt16Type, slot: ClusterSlotType): Action {
|
|
450
451
|
return this.server.action('claimplot', {
|
|
451
452
|
builder_id: UInt64.from(entityId),
|
|
452
453
|
target_item_id: UInt16.from(targetItemId),
|
|
453
|
-
|
|
454
|
+
slot: {hub: UInt64.from(slot.hub), gx: slot.gx, gy: slot.gy},
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
movetile(
|
|
459
|
+
hubId: UInt64Type,
|
|
460
|
+
fromGx: number,
|
|
461
|
+
fromGy: number,
|
|
462
|
+
toGx: number,
|
|
463
|
+
toGy: number
|
|
464
|
+
): Action {
|
|
465
|
+
return this.server.action('movetile', {
|
|
466
|
+
hub_id: UInt64.from(hubId),
|
|
467
|
+
from_gx: fromGx,
|
|
468
|
+
from_gy: fromGy,
|
|
469
|
+
to_gx: toGx,
|
|
470
|
+
to_gy: toGy,
|
|
454
471
|
})
|
|
455
472
|
}
|
|
456
473
|
|
|
@@ -593,6 +610,23 @@ export class ActionsManager extends BaseManager {
|
|
|
593
610
|
)
|
|
594
611
|
}
|
|
595
612
|
|
|
613
|
+
sendAsset(owner: NameType, recipient: NameType, assetId: UInt64Type, memo = ''): Action {
|
|
614
|
+
return Action.from(
|
|
615
|
+
{
|
|
616
|
+
account: this.atomicAssetsAccount,
|
|
617
|
+
name: 'transfer',
|
|
618
|
+
authorization: [{actor: Name.from(owner), permission: 'active'}],
|
|
619
|
+
data: {
|
|
620
|
+
from: Name.from(owner),
|
|
621
|
+
to: Name.from(recipient),
|
|
622
|
+
asset_ids: [UInt64.from(assetId)],
|
|
623
|
+
memo,
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
ATOMICASSETS_ABI
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
|
|
596
630
|
// Two top-level actions the wallet signs to unwrap an NFT into a host's cargo.
|
|
597
631
|
unwrapCargoTx(owner: NameType, assetId: UInt64Type, hostId: UInt64Type): Action[] {
|
|
598
632
|
return [this.transferForUnwrap(owner, assetId), this.placecargo(owner, hostId, assetId)]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {computeFreeCells} from './cluster'
|
|
3
|
+
|
|
4
|
+
describe('computeFreeCells', () => {
|
|
5
|
+
test('returns footprint cells that are not occupied', () => {
|
|
6
|
+
const footprint = [
|
|
7
|
+
{gx: 2, gy: 0},
|
|
8
|
+
{gx: 0, gy: 2},
|
|
9
|
+
{gx: 1, gy: 1},
|
|
10
|
+
]
|
|
11
|
+
const occupied = [{gx: 2, gy: 0, entity: 7}]
|
|
12
|
+
expect(computeFreeCells(footprint, occupied)).toEqual([
|
|
13
|
+
{gx: 0, gy: 2},
|
|
14
|
+
{gx: 1, gy: 1},
|
|
15
|
+
])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('matches occupancy on signed coordinates', () => {
|
|
19
|
+
const footprint = [
|
|
20
|
+
{gx: -2, gy: 0},
|
|
21
|
+
{gx: 0, gy: -2},
|
|
22
|
+
{gx: -1, gy: 1},
|
|
23
|
+
]
|
|
24
|
+
const occupied = [{gx: 0, gy: -2, entity: 9}]
|
|
25
|
+
expect(computeFreeCells(footprint, occupied)).toEqual([
|
|
26
|
+
{gx: -2, gy: 0},
|
|
27
|
+
{gx: -1, gy: 1},
|
|
28
|
+
])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('empty footprint yields no free cells', () => {
|
|
32
|
+
expect(computeFreeCells([], [])).toEqual([])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('fully occupied footprint yields no free cells', () => {
|
|
36
|
+
const footprint = [{gx: 1, gy: 1}]
|
|
37
|
+
expect(computeFreeCells(footprint, [{gx: 1, gy: 1, entity: 3}])).toEqual([])
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {UInt16, type UInt16Type, UInt64, type UInt64Type} from '@wharfkit/antelope'
|
|
2
|
+
import {BaseManager} from './base'
|
|
3
|
+
import type {ServerContract} from '../contracts'
|
|
4
|
+
|
|
5
|
+
export interface GridCell {
|
|
6
|
+
gx: number
|
|
7
|
+
gy: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ClusterCell extends GridCell {
|
|
11
|
+
entity: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Cluster {
|
|
15
|
+
root: number
|
|
16
|
+
cells: ClusterCell[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function computeFreeCells(footprint: GridCell[], occupied: ClusterCell[]): GridCell[] {
|
|
20
|
+
const taken = new Set(occupied.map((c) => `${c.gx},${c.gy}`))
|
|
21
|
+
return footprint.filter((c) => !taken.has(`${c.gx},${c.gy}`))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ClusterManager extends BaseManager {
|
|
25
|
+
async getFootprint(itemId: UInt16Type): Promise<GridCell[]> {
|
|
26
|
+
const res = (await this.server.readonly('getfootprint', {
|
|
27
|
+
item_id: UInt16.from(itemId),
|
|
28
|
+
})) as ServerContract.Types.footprint_result
|
|
29
|
+
return res.cells.map((c) => ({gx: Number(c.gx), gy: Number(c.gy)}))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async getCluster(hubId: UInt64Type): Promise<Cluster> {
|
|
33
|
+
const res = (await this.server.readonly('getcluster', {
|
|
34
|
+
hub_id: UInt64.from(hubId),
|
|
35
|
+
})) as ServerContract.Types.cluster_row
|
|
36
|
+
return {
|
|
37
|
+
root: Number(res.root),
|
|
38
|
+
cells: res.cells.map((c) => ({
|
|
39
|
+
gx: Number(c.gx),
|
|
40
|
+
gy: Number(c.gy),
|
|
41
|
+
entity: Number(c.entity),
|
|
42
|
+
})),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async freeCells(hubId: UInt64Type, hubItemId: UInt16Type): Promise<GridCell[]> {
|
|
47
|
+
const [footprint, cluster] = await Promise.all([
|
|
48
|
+
this.getFootprint(hubItemId),
|
|
49
|
+
this.getCluster(hubId),
|
|
50
|
+
])
|
|
51
|
+
return computeFreeCells(footprint, cluster.cells)
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/managers/context.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {LocationsManager} from './locations'
|
|
|
9
9
|
import {CoordinatesManager} from './coordinates'
|
|
10
10
|
import {EpochsManager} from './epochs'
|
|
11
11
|
import {ActionsManager} from './actions'
|
|
12
|
+
import {ClusterManager} from './cluster'
|
|
12
13
|
import {NftManager} from './nft'
|
|
13
14
|
import {SubscriptionsManager} from '../subscriptions/manager'
|
|
14
15
|
|
|
@@ -19,6 +20,7 @@ export class GameContext {
|
|
|
19
20
|
private _coordinates?: CoordinatesManager
|
|
20
21
|
private _epochs?: EpochsManager
|
|
21
22
|
private _actions?: ActionsManager
|
|
23
|
+
private _clusters?: ClusterManager
|
|
22
24
|
private _nft?: NftManager
|
|
23
25
|
private _subscriptions?: SubscriptionsManager
|
|
24
26
|
private _subscriptionsUrl?: string
|
|
@@ -75,6 +77,13 @@ export class GameContext {
|
|
|
75
77
|
return this._actions
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
get clusters(): ClusterManager {
|
|
81
|
+
if (!this._clusters) {
|
|
82
|
+
this._clusters = new ClusterManager(this)
|
|
83
|
+
}
|
|
84
|
+
return this._clusters
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
get nft(): NftManager {
|
|
79
88
|
if (!this._nft) {
|
|
80
89
|
this._nft = new NftManager(this)
|
package/src/managers/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ export {LocationsManager} from './locations'
|
|
|
8
8
|
export type {LocationStratum} from './locations'
|
|
9
9
|
export {EpochsManager} from './epochs'
|
|
10
10
|
export {ActionsManager} from './actions'
|
|
11
|
+
export {ClusterManager, computeFreeCells} from './cluster'
|
|
12
|
+
export type {GridCell, ClusterCell, Cluster} from './cluster'
|
|
11
13
|
export {NftManager} from './nft'
|
|
12
14
|
export type {NftConfigForItem} from './nft'
|
|
13
15
|
export {PlotManager} from './plot'
|
|
@@ -198,16 +198,14 @@ export function buildModuleImmutable(
|
|
|
198
198
|
}
|
|
199
199
|
case MODULE_GATHERER: {
|
|
200
200
|
const str = decodeStat(stats, 0)
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const ref = decodeStat(stats, 3)
|
|
201
|
+
const hrd = decodeStat(stats, 1)
|
|
202
|
+
const sat = decodeStat(stats, 2)
|
|
204
203
|
base.push({first: 'strength', second: ['uint16', str]})
|
|
205
|
-
base.push({first: '
|
|
206
|
-
base.push({first: 'saturation', second: ['uint16',
|
|
207
|
-
base.push({first: 'plasticity', second: ['uint16', ref]})
|
|
204
|
+
base.push({first: 'hardness', second: ['uint16', hrd]})
|
|
205
|
+
base.push({first: 'saturation', second: ['uint16', sat]})
|
|
208
206
|
base.push({first: 'yield', second: ['uint16', computeGathererYield(str)]})
|
|
209
|
-
base.push({first: 'drain', second: ['uint16', computeGathererDrain(
|
|
210
|
-
base.push({first: 'depth', second: ['uint16', computeGathererDepth(
|
|
207
|
+
base.push({first: 'drain', second: ['uint16', computeGathererDrain(sat)]})
|
|
208
|
+
base.push({first: 'depth', second: ['uint16', computeGathererDepth(hrd, item.tier)]})
|
|
211
209
|
break
|
|
212
210
|
}
|
|
213
211
|
case MODULE_LOADER: {
|
|
@@ -220,17 +218,17 @@ export function buildModuleImmutable(
|
|
|
220
218
|
break
|
|
221
219
|
}
|
|
222
220
|
case MODULE_WARP: {
|
|
223
|
-
const
|
|
224
|
-
base.push({first: '
|
|
225
|
-
base.push({first: 'range', second: ['uint32', computeWarpRange(
|
|
221
|
+
const ref = decodeStat(stats, 0)
|
|
222
|
+
base.push({first: 'reflectivity', second: ['uint16', ref]})
|
|
223
|
+
base.push({first: 'range', second: ['uint32', computeWarpRange(ref)]})
|
|
226
224
|
break
|
|
227
225
|
}
|
|
228
226
|
case MODULE_CRAFTER: {
|
|
229
|
-
const
|
|
227
|
+
const fin = decodeStat(stats, 0)
|
|
230
228
|
const con = decodeStat(stats, 1)
|
|
231
|
-
base.push({first: '
|
|
229
|
+
base.push({first: 'fineness', second: ['uint16', fin]})
|
|
232
230
|
base.push({first: 'conductivity', second: ['uint16', con]})
|
|
233
|
-
base.push({first: 'speed', second: ['uint16', computeCrafterSpeed(
|
|
231
|
+
base.push({first: 'speed', second: ['uint16', computeCrafterSpeed(fin)]})
|
|
234
232
|
base.push({first: 'drain', second: ['uint16', computeCrafterDrain(con)]})
|
|
235
233
|
break
|
|
236
234
|
}
|
|
@@ -267,13 +265,13 @@ export function buildModuleImmutable(
|
|
|
267
265
|
case MODULE_HAULER: {
|
|
268
266
|
const res = decodeStat(stats, 0)
|
|
269
267
|
const pla = decodeStat(stats, 1)
|
|
270
|
-
const
|
|
268
|
+
const con = decodeStat(stats, 2)
|
|
271
269
|
base.push({first: 'resonance', second: ['uint16', res]})
|
|
272
270
|
base.push({first: 'plasticity', second: ['uint16', pla]})
|
|
273
|
-
base.push({first: '
|
|
271
|
+
base.push({first: 'conductivity', second: ['uint16', con]})
|
|
274
272
|
base.push({first: 'capacity', second: ['uint8', computeHaulerCapacity(res)]})
|
|
275
273
|
base.push({first: 'efficiency', second: ['uint16', computeHaulerEfficiency(pla)]})
|
|
276
|
-
base.push({first: 'drain', second: ['uint16', computeHaulerDrain(
|
|
274
|
+
base.push({first: 'drain', second: ['uint16', computeHaulerDrain(con)]})
|
|
277
275
|
break
|
|
278
276
|
}
|
|
279
277
|
}
|
package/src/nft/description.ts
CHANGED
|
@@ -107,7 +107,7 @@ export function entityDisplayName(itemId: number): string {
|
|
|
107
107
|
case ITEM_WAREHOUSE_T1_PACKED:
|
|
108
108
|
return 'Warehouse'
|
|
109
109
|
case ITEM_EXTRACTOR_T1_PACKED:
|
|
110
|
-
return '
|
|
110
|
+
return 'Mining Rig'
|
|
111
111
|
case ITEM_FACTORY_T1_PACKED:
|
|
112
112
|
return 'Factory'
|
|
113
113
|
case ITEM_CONTAINER_T1_PACKED:
|
|
@@ -124,19 +124,19 @@ export function moduleDisplayName(itemId: number): string {
|
|
|
124
124
|
case ITEM_ENGINE_T1:
|
|
125
125
|
return 'Engine'
|
|
126
126
|
case ITEM_GENERATOR_T1:
|
|
127
|
-
return '
|
|
127
|
+
return 'Reactor'
|
|
128
128
|
case ITEM_GATHERER_T1:
|
|
129
|
-
return '
|
|
129
|
+
return 'Limpet Bay'
|
|
130
130
|
case ITEM_LOADER_T1:
|
|
131
|
-
return '
|
|
131
|
+
return 'Shuttle Bay'
|
|
132
132
|
case ITEM_CRAFTER_T1:
|
|
133
|
-
return '
|
|
133
|
+
return 'Fabricator'
|
|
134
134
|
case ITEM_STORAGE_T1:
|
|
135
|
-
return 'Cargo
|
|
135
|
+
return 'Cargo Hold'
|
|
136
136
|
case ITEM_HAULER_T1:
|
|
137
|
-
return '
|
|
137
|
+
return 'Tractor Beam'
|
|
138
138
|
case ITEM_WARP_T1:
|
|
139
|
-
return 'Warp'
|
|
139
|
+
return 'Warp Drive'
|
|
140
140
|
case ITEM_BATTERY_T1:
|
|
141
141
|
return 'Battery Bank'
|
|
142
142
|
default:
|
|
@@ -61,8 +61,8 @@ const TEMPLATES: Record<string, TemplateSpec> = {
|
|
|
61
61
|
],
|
|
62
62
|
highlightKeys: ['yield', 'depth', 'drain'],
|
|
63
63
|
},
|
|
64
|
-
|
|
65
|
-
id: 'module.
|
|
64
|
+
loading: {
|
|
65
|
+
id: 'module.loading.description',
|
|
66
66
|
template: 'generates {thrust} thrust with a weight of {mass} per unit',
|
|
67
67
|
params: [
|
|
68
68
|
['thrust', 'Thrust'],
|
|
@@ -70,8 +70,8 @@ const TEMPLATES: Record<string, TemplateSpec> = {
|
|
|
70
70
|
],
|
|
71
71
|
highlightKeys: ['thrust', 'mass'],
|
|
72
72
|
},
|
|
73
|
-
|
|
74
|
-
id: 'module.
|
|
73
|
+
crafting: {
|
|
74
|
+
id: 'module.crafting.description',
|
|
75
75
|
template: 'manufactures items at {speed} speed while draining {drain} energy per second',
|
|
76
76
|
params: [
|
|
77
77
|
['speed', 'Speed'],
|
|
@@ -91,8 +91,8 @@ const TEMPLATES: Record<string, TemplateSpec> = {
|
|
|
91
91
|
params: [['capacity', 'Energy Capacity']],
|
|
92
92
|
highlightKeys: ['capacity'],
|
|
93
93
|
},
|
|
94
|
-
|
|
95
|
-
id: 'module.
|
|
94
|
+
hauling: {
|
|
95
|
+
id: 'module.hauling.description',
|
|
96
96
|
template:
|
|
97
97
|
'locks onto up to {capacity} targets at {efficiency} efficiency while draining {drain} energy per distance travelled per target',
|
|
98
98
|
params: [
|
|
@@ -198,7 +198,7 @@ function computeCapabilityGroup(
|
|
|
198
198
|
case MODULE_LOADER: {
|
|
199
199
|
const caps = computeLoaderCapabilities(stats)
|
|
200
200
|
return {
|
|
201
|
-
capability: '
|
|
201
|
+
capability: 'Loading',
|
|
202
202
|
attributes: [
|
|
203
203
|
{label: 'Mass', value: caps.mass},
|
|
204
204
|
{label: 'Thrust', value: caps.thrust},
|
|
@@ -209,7 +209,7 @@ function computeCapabilityGroup(
|
|
|
209
209
|
case MODULE_CRAFTER: {
|
|
210
210
|
const caps = computeCrafterCapabilities(stats)
|
|
211
211
|
return {
|
|
212
|
-
capability: '
|
|
212
|
+
capability: 'Crafting',
|
|
213
213
|
attributes: [
|
|
214
214
|
{label: 'Speed', value: caps.speed},
|
|
215
215
|
{label: 'Drain', value: caps.drain},
|
|
@@ -219,7 +219,7 @@ function computeCapabilityGroup(
|
|
|
219
219
|
case MODULE_HAULER: {
|
|
220
220
|
const caps = computeHaulerCapabilities(stats)
|
|
221
221
|
return {
|
|
222
|
-
capability: '
|
|
222
|
+
capability: 'Hauling',
|
|
223
223
|
attributes: [
|
|
224
224
|
{label: 'Capacity', value: caps.capacity},
|
|
225
225
|
{label: 'Efficiency', value: caps.efficiency},
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
derivedLoaders,
|
|
4
|
+
unwrapTransitDuration,
|
|
5
|
+
unwrapLoadDuration,
|
|
6
|
+
estimateUnwrapDuration,
|
|
7
|
+
incomingHoldMass,
|
|
8
|
+
projectedPeakCargomass,
|
|
9
|
+
} from './unwrap'
|
|
10
|
+
|
|
11
|
+
describe('unwrap duration mirror', () => {
|
|
12
|
+
test('derivedLoaders aggregates lanes like derived_loaders()', () => {
|
|
13
|
+
expect(derivedLoaders([])).toBeNull()
|
|
14
|
+
expect(
|
|
15
|
+
derivedLoaders([
|
|
16
|
+
{mass: 1000, thrust: 10},
|
|
17
|
+
{mass: 1400, thrust: 20},
|
|
18
|
+
])
|
|
19
|
+
).toEqual({mass: 1200, thrust: 30, quantity: 2}) // floor(2400/2)=1200, sum thrust, count
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('transit floors distance then flight time', () => {
|
|
23
|
+
// distance = floor(sqrt(3^2+4^2)*10000)=50000; accel=400/mass*10000; flight=floor(2*sqrt(d/accel))
|
|
24
|
+
const mass = 1000
|
|
25
|
+
const accel = (400 / mass) * 10000
|
|
26
|
+
const expected = Math.floor(2 * Math.sqrt(50000 / accel))
|
|
27
|
+
expect(unwrapTransitDuration(mass, {x: 0, y: 0}, {x: 3, y: 4})).toBe(expected)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('load uses altitude z, adds loader mass, divides by quantity', () => {
|
|
31
|
+
const loaders = {mass: 1200, thrust: 30, quantity: 2}
|
|
32
|
+
const itemMass = 800
|
|
33
|
+
const accel = (30 / (itemMass + 1200)) * 10000
|
|
34
|
+
const flight = Math.floor(2 * Math.sqrt(3000 / accel))
|
|
35
|
+
expect(unwrapLoadDuration(loaders, itemMass, 3000)).toBe(Math.floor(flight / 2))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('zero item mass and no loaders are safe', () => {
|
|
39
|
+
expect(unwrapTransitDuration(0, {x: 0, y: 0}, {x: 9, y: 9})).toBe(0)
|
|
40
|
+
expect(unwrapLoadDuration(null, 500, 3000)).toBe(0)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('incomingHoldMass sums incoming-kind hold mass', () => {
|
|
45
|
+
expect(incomingHoldMass([])).toBe(0)
|
|
46
|
+
// PUSH(2) + FLIGHT(5) count; BUILD(4) does not
|
|
47
|
+
expect(
|
|
48
|
+
incomingHoldMass([
|
|
49
|
+
{kind: 2, incoming_mass: 100},
|
|
50
|
+
{kind: 4, incoming_mass: 999},
|
|
51
|
+
{kind: 5, incoming_mass: 50},
|
|
52
|
+
])
|
|
53
|
+
).toBe(150)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('projectedPeakCargomass tracks the running peak from cargomass', () => {
|
|
57
|
+
const entity = {cargomass: 1000, lanes: [], cargo: [], schedule: undefined} as never
|
|
58
|
+
// No pending tasks: peak = base + candidate add.
|
|
59
|
+
expect(projectedPeakCargomass(entity, new Date(0), 500)).toBe(1500)
|
|
60
|
+
})
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type {UInt16Type, UInt32Type} from '@wharfkit/antelope'
|
|
2
|
+
import {calcCargoItemMass} from '../capabilities/storage'
|
|
3
|
+
import type {ServerContract} from '../contracts'
|
|
4
|
+
import {PRECISION} from '../types'
|
|
5
|
+
import * as sched from './schedule'
|
|
6
|
+
import {taskCargoEffect} from './availability'
|
|
7
|
+
import {candidateLaneCompletesAt} from './lanes'
|
|
8
|
+
|
|
9
|
+
const NFT_TRANSIT_THRUST = 400
|
|
10
|
+
|
|
11
|
+
export interface DerivedLoaders {
|
|
12
|
+
mass: number
|
|
13
|
+
thrust: number
|
|
14
|
+
quantity: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function derivedLoaders(
|
|
18
|
+
lanes: {mass: UInt32Type | number; thrust: UInt16Type | number}[] | undefined
|
|
19
|
+
): DerivedLoaders | null {
|
|
20
|
+
if (!lanes || lanes.length === 0) return null
|
|
21
|
+
let totalMass = 0
|
|
22
|
+
let totalThrust = 0
|
|
23
|
+
for (const l of lanes) {
|
|
24
|
+
totalMass += Number(l.mass)
|
|
25
|
+
totalThrust += Number(l.thrust)
|
|
26
|
+
}
|
|
27
|
+
const count = lanes.length
|
|
28
|
+
return {
|
|
29
|
+
mass: Math.floor(totalMass / count),
|
|
30
|
+
thrust: Math.min(totalThrust, 65_535),
|
|
31
|
+
quantity: count,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function acceleration(thrust: number, mass: number): number {
|
|
36
|
+
if (mass <= 0) return 0
|
|
37
|
+
return (thrust / mass) * PRECISION
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function flightTime(distance: number, accel: number): number {
|
|
41
|
+
if (accel <= 0 || distance <= 0) return 0
|
|
42
|
+
return Math.floor(2 * Math.sqrt(distance / accel))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function distance2d(ax: number, ay: number, bx: number, by: number): number {
|
|
46
|
+
const dx = ax - bx
|
|
47
|
+
const dy = ay - by
|
|
48
|
+
return Math.floor(Math.sqrt(dx * dx + dy * dy) * PRECISION)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function unwrapTransitDuration(
|
|
52
|
+
itemMass: number,
|
|
53
|
+
origin: {x: number; y: number},
|
|
54
|
+
dest: {x: number; y: number}
|
|
55
|
+
): number {
|
|
56
|
+
if (itemMass <= 0) return 0
|
|
57
|
+
return flightTime(
|
|
58
|
+
distance2d(origin.x, origin.y, dest.x, dest.y),
|
|
59
|
+
acceleration(NFT_TRANSIT_THRUST, itemMass)
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function unwrapLoadDuration(
|
|
64
|
+
loaders: DerivedLoaders | null,
|
|
65
|
+
itemMass: number,
|
|
66
|
+
destZ: number
|
|
67
|
+
): number {
|
|
68
|
+
if (!loaders || itemMass <= 0) return 0
|
|
69
|
+
const total = itemMass + loaders.mass
|
|
70
|
+
const flight = flightTime(destZ, acceleration(loaders.thrust, total))
|
|
71
|
+
return Math.floor(flight / loaders.quantity)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface UnwrapItem {
|
|
75
|
+
itemId: number
|
|
76
|
+
quantity: number
|
|
77
|
+
modules: ServerContract.Types.module_entry[]
|
|
78
|
+
originX: number
|
|
79
|
+
originY: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface UnwrapDestination {
|
|
83
|
+
loader_lanes?: {mass: UInt32Type | number; thrust: UInt16Type | number}[]
|
|
84
|
+
coordinates: {x: UInt32Type | number; y: UInt32Type | number; z?: UInt32Type | number}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function estimateUnwrapDuration(dest: UnwrapDestination, item: UnwrapItem): number {
|
|
88
|
+
const itemMass = Number(
|
|
89
|
+
calcCargoItemMass({
|
|
90
|
+
item_id: item.itemId as never,
|
|
91
|
+
quantity: item.quantity as never,
|
|
92
|
+
modules: item.modules,
|
|
93
|
+
})
|
|
94
|
+
)
|
|
95
|
+
const loaders = derivedLoaders(dest.loader_lanes)
|
|
96
|
+
const dz = Number(dest.coordinates.z ?? 0)
|
|
97
|
+
const load = unwrapLoadDuration(loaders, itemMass, dz)
|
|
98
|
+
const transit = unwrapTransitDuration(
|
|
99
|
+
itemMass,
|
|
100
|
+
{x: item.originX, y: item.originY},
|
|
101
|
+
{
|
|
102
|
+
x: Number(dest.coordinates.x),
|
|
103
|
+
y: Number(dest.coordinates.y),
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
return load + transit
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Hold kinds that count as incoming (mirror is_incoming_hold_kind in holds.hpp).
|
|
110
|
+
const INCOMING_HOLD_KINDS = new Set<number>([2, 3, 5])
|
|
111
|
+
|
|
112
|
+
export function incomingHoldMass(
|
|
113
|
+
holds:
|
|
114
|
+
| {kind: number | {toNumber(): number}; incoming_mass: number | {toNumber(): number}}[]
|
|
115
|
+
| undefined
|
|
116
|
+
): number {
|
|
117
|
+
if (!holds) return 0
|
|
118
|
+
let total = 0
|
|
119
|
+
for (const h of holds) {
|
|
120
|
+
if (INCOMING_HOLD_KINDS.has(Number(h.kind))) total += Number(h.incoming_mass)
|
|
121
|
+
}
|
|
122
|
+
return total
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type CargoItem = ServerContract.Types.cargo_item
|
|
126
|
+
|
|
127
|
+
function cargoListMass(items: CargoItem[]): number {
|
|
128
|
+
let m = 0
|
|
129
|
+
for (const it of items) m += Number(calcCargoItemMass(it))
|
|
130
|
+
return m
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function projectedPeakCargomass(
|
|
134
|
+
entity: sched.ScheduleData & {cargomass: number | {toNumber(): number}},
|
|
135
|
+
at: Date,
|
|
136
|
+
addMass: number,
|
|
137
|
+
removeMass = 0
|
|
138
|
+
): number {
|
|
139
|
+
const events: {t: number; delta: number}[] = []
|
|
140
|
+
for (const ordered of sched.orderedTasks(entity)) {
|
|
141
|
+
const eff = taskCargoEffect(ordered.task)
|
|
142
|
+
const delta = cargoListMass(eff.added) - cargoListMass(eff.removed)
|
|
143
|
+
events.push({t: ordered.completesAt.getTime(), delta})
|
|
144
|
+
}
|
|
145
|
+
events.push({t: at.getTime(), delta: addMass - removeMass})
|
|
146
|
+
events.sort((a, b) => (a.t !== b.t ? a.t - b.t : b.delta - a.delta))
|
|
147
|
+
let running = Number(entity.cargomass)
|
|
148
|
+
let peak = running
|
|
149
|
+
for (const e of events) {
|
|
150
|
+
running += e.delta
|
|
151
|
+
if (running < 0) running = 0
|
|
152
|
+
if (running > peak) peak = running
|
|
153
|
+
}
|
|
154
|
+
return Math.min(peak, 0xffff_ffff)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function receiveFits(
|
|
158
|
+
dest: UnwrapDestination &
|
|
159
|
+
sched.ScheduleData & {
|
|
160
|
+
cargomass: number | {toNumber(): number}
|
|
161
|
+
capacity?: number | {toNumber(): number}
|
|
162
|
+
holds?: {
|
|
163
|
+
kind: number | {toNumber(): number}
|
|
164
|
+
incoming_mass: number | {toNumber(): number}
|
|
165
|
+
}[]
|
|
166
|
+
},
|
|
167
|
+
item: UnwrapItem,
|
|
168
|
+
now: Date
|
|
169
|
+
): boolean {
|
|
170
|
+
const capacity = Number(dest.capacity ?? 0)
|
|
171
|
+
if (capacity <= 0) return false
|
|
172
|
+
const itemMass = Number(
|
|
173
|
+
calcCargoItemMass({
|
|
174
|
+
item_id: item.itemId as never,
|
|
175
|
+
quantity: item.quantity as never,
|
|
176
|
+
modules: item.modules,
|
|
177
|
+
})
|
|
178
|
+
)
|
|
179
|
+
const duration = estimateUnwrapDuration(dest, item)
|
|
180
|
+
const candidateCompletes = candidateLaneCompletesAt(dest, sched.LANE_MOBILITY, duration, now)
|
|
181
|
+
const peak = projectedPeakCargomass(
|
|
182
|
+
dest,
|
|
183
|
+
candidateCompletes,
|
|
184
|
+
itemMass + incomingHoldMass(dest.holds)
|
|
185
|
+
)
|
|
186
|
+
return peak <= capacity
|
|
187
|
+
}
|
package/src/shipload.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {LocationsManager} from './managers/locations'
|
|
|
10
10
|
import type {CoordinatesManager} from './managers/coordinates'
|
|
11
11
|
import type {EpochsManager} from './managers/epochs'
|
|
12
12
|
import type {ActionsManager} from './managers/actions'
|
|
13
|
+
import type {ClusterManager} from './managers/cluster'
|
|
13
14
|
import type {NftManager} from './managers/nft'
|
|
14
15
|
import type {SubscriptionsManager} from './subscriptions/manager'
|
|
15
16
|
import type {GameState} from './entities/gamestate'
|
|
@@ -123,6 +124,10 @@ export class Shipload {
|
|
|
123
124
|
return this._context.actions
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
get clusters(): ClusterManager {
|
|
128
|
+
return this._context.clusters
|
|
129
|
+
}
|
|
130
|
+
|
|
126
131
|
get nft(): NftManager {
|
|
127
132
|
return this._context.nft
|
|
128
133
|
}
|