@shipload/sdk 1.0.0-next.4 → 1.0.0-next.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/scan.d.ts +34 -0
- package/lib/scan.js +136 -0
- package/lib/scan.js.map +1 -0
- package/lib/scan.m.js +129 -0
- package/lib/scan.m.js.map +1 -0
- package/lib/shipload.d.ts +2473 -973
- package/lib/shipload.js +11529 -5211
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +11338 -5162
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +970 -0
- package/lib/testing.js +4013 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +4007 -0
- package/lib/testing.m.js.map +1 -0
- package/package.json +20 -2
- package/src/capabilities/craftable.ts +51 -0
- package/src/capabilities/crafting.test.ts +7 -0
- package/src/capabilities/crafting.ts +5 -6
- package/src/capabilities/gathering.test.ts +16 -0
- package/src/capabilities/gathering.ts +35 -18
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +9 -0
- package/src/capabilities/storage.ts +16 -1
- package/src/contracts/platform.ts +231 -3
- package/src/contracts/server.ts +1021 -481
- package/src/coordinates/address.ts +88 -0
- package/src/coordinates/constants.test.ts +15 -0
- package/src/coordinates/constants.ts +23 -0
- package/src/coordinates/index.ts +15 -0
- package/src/coordinates/memo.test.ts +47 -0
- package/src/coordinates/memo.ts +20 -0
- package/src/coordinates/permutation.ts +77 -0
- package/src/coordinates/regions.ts +48 -0
- package/src/coordinates/sectors.ts +115 -0
- package/src/data/capabilities.ts +12 -5
- package/src/data/capability-formulas.ts +14 -7
- package/src/data/catalog.ts +0 -5
- package/src/data/colors.ts +14 -47
- package/src/data/entities.json +76 -10
- package/src/data/item-ids.ts +18 -12
- package/src/data/items.json +321 -38
- package/src/data/kind-registry.json +109 -0
- package/src/data/kind-registry.ts +165 -0
- package/src/data/metadata.ts +119 -33
- package/src/data/recipes-runtime.ts +3 -23
- package/src/data/recipes.json +238 -117
- package/src/derivation/build-methods.ts +45 -0
- package/src/derivation/capabilities.test.ts +151 -0
- package/src/derivation/capabilities.ts +512 -0
- package/src/derivation/capability-mappings.ts +9 -12
- package/src/derivation/crafting.ts +23 -24
- package/src/derivation/index.ts +25 -2
- package/src/derivation/recipe-usage.test.ts +78 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +125 -38
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +56 -0
- package/src/derivation/stars.test.ts +51 -0
- package/src/derivation/stars.ts +15 -0
- package/src/derivation/stats.ts +6 -6
- package/src/derivation/stratum.ts +17 -20
- package/src/derivation/tiers.ts +40 -7
- package/src/derivation/wormhole.ts +136 -0
- package/src/entities/entity.ts +98 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +124 -134
- package/src/entities/slot-multiplier.ts +43 -0
- package/src/errors.ts +12 -16
- package/src/format.ts +26 -4
- package/src/index-module.ts +267 -47
- package/src/managers/actions.ts +528 -95
- package/src/managers/base.ts +6 -2
- package/src/managers/construction-types.ts +80 -0
- package/src/managers/construction.ts +412 -0
- package/src/managers/context.ts +20 -1
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/entities.ts +18 -66
- package/src/managers/epochs.ts +40 -0
- package/src/managers/index.ts +17 -1
- package/src/managers/locations.ts +25 -29
- package/src/managers/nft.test.ts +14 -0
- package/src/managers/nft.ts +70 -0
- package/src/managers/plot.ts +122 -0
- package/src/nft/atomicassets.abi.json +1342 -0
- package/src/nft/atomicassets.ts +237 -0
- package/src/nft/atomicdata.ts +130 -0
- package/src/nft/buildImmutableData.ts +338 -0
- package/src/nft/description.ts +98 -24
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +127 -0
- package/src/planner/planner.test.ts +319 -0
- package/src/resolution/describe-module.ts +18 -13
- package/src/resolution/display-name.ts +38 -10
- package/src/resolution/resolve-item.test.ts +37 -0
- package/src/resolution/resolve-item.ts +55 -24
- package/src/scan/index.ts +180 -0
- package/src/scan/scan-wasm.base64.ts +2 -0
- package/src/scheduling/accessor.ts +68 -22
- package/src/scheduling/availability.ts +108 -0
- package/src/scheduling/cancel.test.ts +348 -0
- package/src/scheduling/cancel.ts +209 -0
- package/src/scheduling/energy.ts +47 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +249 -0
- package/src/scheduling/lanes.ts +198 -0
- package/src/scheduling/projection.ts +209 -105
- package/src/scheduling/schedule.ts +241 -104
- package/src/scheduling/task-cargo.ts +46 -0
- package/src/shipload.ts +21 -1
- package/src/subscriptions/manager.ts +229 -142
- package/src/subscriptions/mappers.ts +5 -8
- package/src/subscriptions/types.ts +11 -3
- package/src/testing/catalog-hash.ts +19 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/projection-parity.ts +167 -0
- package/src/travel/reach.ts +23 -0
- package/src/travel/route-planner.ts +196 -0
- package/src/travel/travel.ts +200 -112
- package/src/types/capabilities.ts +29 -6
- package/src/types/entity.ts +3 -3
- package/src/types/index.ts +0 -1
- package/src/types.ts +28 -13
- package/src/utils/cargo.ts +27 -0
- package/src/utils/display-name.ts +70 -0
- package/src/utils/system.ts +36 -24
- package/src/capabilities/loading.ts +0 -8
- package/src/entities/container.ts +0 -108
- package/src/entities/ship-deploy.ts +0 -259
- package/src/entities/ship.ts +0 -204
- package/src/entities/warehouse.ts +0 -119
- package/src/types/entity-traits.ts +0 -69
package/src/nft/description.ts
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
MODULE_ENGINE,
|
|
5
5
|
MODULE_GATHERER,
|
|
6
6
|
MODULE_GENERATOR,
|
|
7
|
+
MODULE_BATTERY,
|
|
8
|
+
MODULE_HAULER,
|
|
7
9
|
MODULE_LOADER,
|
|
8
10
|
MODULE_STORAGE,
|
|
9
11
|
MODULE_WARP,
|
|
@@ -13,8 +15,12 @@ import {
|
|
|
13
15
|
ITEM_CONTAINER_T2_PACKED,
|
|
14
16
|
ITEM_CRAFTER_T1,
|
|
15
17
|
ITEM_ENGINE_T1,
|
|
18
|
+
ITEM_EXTRACTOR_T1_PACKED,
|
|
19
|
+
ITEM_FACTORY_T1_PACKED,
|
|
16
20
|
ITEM_GATHERER_T1,
|
|
17
21
|
ITEM_GENERATOR_T1,
|
|
22
|
+
ITEM_HAULER_T1,
|
|
23
|
+
ITEM_BATTERY_T1,
|
|
18
24
|
ITEM_LOADER_T1,
|
|
19
25
|
ITEM_SHIP_T1_PACKED,
|
|
20
26
|
ITEM_STORAGE_T1,
|
|
@@ -22,6 +28,8 @@ import {
|
|
|
22
28
|
ITEM_WARP_T1,
|
|
23
29
|
} from '../data/item-ids'
|
|
24
30
|
import {decodeStat} from '../derivation/crafting'
|
|
31
|
+
import {gathererDepthForTier} from '../derivation/capabilities'
|
|
32
|
+
import {getItem} from '../data/catalog'
|
|
25
33
|
|
|
26
34
|
function idiv(a: number, b: number): number {
|
|
27
35
|
return Math.floor(a / b)
|
|
@@ -29,33 +37,68 @@ function idiv(a: number, b: number): number {
|
|
|
29
37
|
|
|
30
38
|
export function computeBaseHullmass(stats: bigint): number {
|
|
31
39
|
const density = decodeStat(stats, 1)
|
|
32
|
-
return
|
|
40
|
+
return 100000 - 75 * density
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
export function computeBaseCapacityShip(stats: bigint): number {
|
|
36
|
-
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
37
|
-
return Math.floor(
|
|
44
|
+
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
45
|
+
return Math.floor(5_000_000 * 6 ** (s / 1998))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function computeBaseCapacityContainer(stats: bigint): number {
|
|
49
|
+
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
50
|
+
return Math.floor(22_000_000 * 6 ** (s / 1998))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function computeBaseCapacityContainerT2(stats: bigint): number {
|
|
54
|
+
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
55
|
+
return Math.floor(24_000_000 * 6 ** (s / 2947))
|
|
38
56
|
}
|
|
39
57
|
|
|
40
58
|
export function computeBaseCapacityWarehouse(stats: bigint): number {
|
|
41
|
-
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
42
|
-
return Math.floor(
|
|
59
|
+
const s = decodeStat(stats, 0) + decodeStat(stats, 2)
|
|
60
|
+
return Math.floor(100_000_000 * 6 ** (s / 1998))
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
export const computeEngineThrust = (vol: number): number => 400 + idiv(vol * 3, 4)
|
|
46
|
-
export const computeEngineDrain = (thm: number): number => Math.max(30, 50 - idiv(thm, 70))
|
|
47
|
-
export const
|
|
48
|
-
export const
|
|
64
|
+
export const computeEngineDrain = (thm: number): number => 2 * Math.max(30, 50 - idiv(thm, 70))
|
|
65
|
+
export const ENGINE_DRAIN_BASE = 118
|
|
66
|
+
export const ENGINE_DRAIN_REF_THRUST = 775
|
|
67
|
+
export const ENGINE_DRAIN_REF_THM = 500
|
|
68
|
+
|
|
69
|
+
export const computeTravelDrain = (totalThrust: number, avgThm: number): number => {
|
|
70
|
+
if (totalThrust <= 0) return 0
|
|
71
|
+
const num = ENGINE_DRAIN_BASE * ENGINE_DRAIN_REF_THRUST * computeEngineDrain(avgThm)
|
|
72
|
+
const den = totalThrust * computeEngineDrain(ENGINE_DRAIN_REF_THM)
|
|
73
|
+
return idiv(num, den)
|
|
74
|
+
}
|
|
75
|
+
export const computeGeneratorCap = (com: number): number => 950 + idiv(com, 2)
|
|
76
|
+
export const computeGeneratorRech = (fin: number): number => 2 * (1 + idiv(fin * 3, 1000))
|
|
49
77
|
export const computeGathererYield = (str: number): number => 200 + str
|
|
50
78
|
export const computeGathererDrain = (con: number): number =>
|
|
51
|
-
Math.max(250, 1250 - idiv(con * 25, 20))
|
|
52
|
-
export const computeGathererDepth = (tol: number): number =>
|
|
53
|
-
|
|
79
|
+
2 * Math.max(250, 1250 - idiv(con * 25, 20))
|
|
80
|
+
export const computeGathererDepth = (tol: number, tier: number): number =>
|
|
81
|
+
gathererDepthForTier(tol, tier)
|
|
54
82
|
export const computeLoaderMass = (ins: number): number => Math.max(200, 2000 - ins * 2)
|
|
55
|
-
export const computeLoaderThrust = (pla: number): number => 1 + idiv(pla,
|
|
83
|
+
export const computeLoaderThrust = (pla: number): number => 1 + idiv(pla * pla, 10000)
|
|
56
84
|
export const computeCrafterSpeed = (rea: number): number => 100 + idiv(rea * 4, 5)
|
|
57
85
|
export const computeCrafterDrain = (fin: number): number => Math.max(5, 30 - idiv(fin, 33))
|
|
86
|
+
export const computeHaulerCapacity = (fin: number): number => Math.max(1, 1 + idiv(fin, 400))
|
|
87
|
+
export const computeHaulerEfficiency = (con: number): number => 2000 + con * 6
|
|
88
|
+
export const computeHaulerDrain = (com: number): number => Math.max(3, 15 - idiv(com, 80))
|
|
58
89
|
export const computeWarpRange = (stat: number): number => 100 + stat * 3
|
|
90
|
+
export const computeCargoBayCapacity = (
|
|
91
|
+
strength: number,
|
|
92
|
+
density: number,
|
|
93
|
+
hardness: number,
|
|
94
|
+
cohesion: number
|
|
95
|
+
): number => 10_000_000 + idiv((strength + density + hardness + cohesion) * 50_000_000, 3996)
|
|
96
|
+
export const computeBatteryBankCapacity = (
|
|
97
|
+
volatility: number,
|
|
98
|
+
thermal: number,
|
|
99
|
+
plasticity: number,
|
|
100
|
+
insulation: number
|
|
101
|
+
): number => 2_500 + idiv((volatility + thermal + plasticity + insulation) * 7_500, 3996)
|
|
59
102
|
|
|
60
103
|
export function entityDisplayName(itemId: number): string {
|
|
61
104
|
switch (itemId) {
|
|
@@ -63,6 +106,10 @@ export function entityDisplayName(itemId: number): string {
|
|
|
63
106
|
return 'Ship'
|
|
64
107
|
case ITEM_WAREHOUSE_T1_PACKED:
|
|
65
108
|
return 'Warehouse'
|
|
109
|
+
case ITEM_EXTRACTOR_T1_PACKED:
|
|
110
|
+
return 'Extractor'
|
|
111
|
+
case ITEM_FACTORY_T1_PACKED:
|
|
112
|
+
return 'Factory'
|
|
66
113
|
case ITEM_CONTAINER_T1_PACKED:
|
|
67
114
|
return 'Container'
|
|
68
115
|
case ITEM_CONTAINER_T2_PACKED:
|
|
@@ -85,9 +132,13 @@ export function moduleDisplayName(itemId: number): string {
|
|
|
85
132
|
case ITEM_CRAFTER_T1:
|
|
86
133
|
return 'Crafter'
|
|
87
134
|
case ITEM_STORAGE_T1:
|
|
88
|
-
return '
|
|
135
|
+
return 'Cargo Bay'
|
|
136
|
+
case ITEM_HAULER_T1:
|
|
137
|
+
return 'Hauler'
|
|
89
138
|
case ITEM_WARP_T1:
|
|
90
139
|
return 'Warp'
|
|
140
|
+
case ITEM_BATTERY_T1:
|
|
141
|
+
return 'Battery Bank'
|
|
91
142
|
default:
|
|
92
143
|
return 'Module'
|
|
93
144
|
}
|
|
@@ -119,11 +170,12 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
|
|
|
119
170
|
case MODULE_GATHERER: {
|
|
120
171
|
const str = decodeStat(stats, 0)
|
|
121
172
|
const tol = decodeStat(stats, 1)
|
|
122
|
-
const con = decodeStat(stats,
|
|
123
|
-
const
|
|
173
|
+
const con = decodeStat(stats, 2)
|
|
174
|
+
const tier = getItem(itemId).tier
|
|
124
175
|
out += ` Yield ${computeGathererYield(str)} Depth ${computeGathererDepth(
|
|
125
|
-
tol
|
|
126
|
-
|
|
176
|
+
tol,
|
|
177
|
+
tier
|
|
178
|
+
)} Drain ${computeGathererDrain(con)}`
|
|
127
179
|
break
|
|
128
180
|
}
|
|
129
181
|
case MODULE_LOADER: {
|
|
@@ -134,17 +186,23 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
|
|
|
134
186
|
}
|
|
135
187
|
case MODULE_CRAFTER: {
|
|
136
188
|
const rea = decodeStat(stats, 0)
|
|
137
|
-
const
|
|
138
|
-
out += ` Speed ${computeCrafterSpeed(rea)} Drain ${computeCrafterDrain(
|
|
189
|
+
const con = decodeStat(stats, 1)
|
|
190
|
+
out += ` Speed ${computeCrafterSpeed(rea)} Drain ${computeCrafterDrain(con)}`
|
|
139
191
|
break
|
|
140
192
|
}
|
|
141
193
|
case MODULE_STORAGE: {
|
|
142
194
|
const str = decodeStat(stats, 0)
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
195
|
+
const den = decodeStat(stats, 1)
|
|
196
|
+
const hrd = decodeStat(stats, 2)
|
|
197
|
+
const com = decodeStat(stats, 3)
|
|
198
|
+
out += ` Cargo Capacity ${computeCargoBayCapacity(str, den, hrd, com)}`
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
case MODULE_HAULER: {
|
|
202
|
+
const res = decodeStat(stats, 0)
|
|
203
|
+
const pla = decodeStat(stats, 1)
|
|
204
|
+
const ref = decodeStat(stats, 2)
|
|
205
|
+
out += ` Capacity ${computeHaulerCapacity(res)} Efficiency ${computeHaulerEfficiency(pla)} Drain ${computeHaulerDrain(ref)}`
|
|
148
206
|
break
|
|
149
207
|
}
|
|
150
208
|
case MODULE_WARP: {
|
|
@@ -152,6 +210,14 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
|
|
|
152
210
|
out += ` Range ${computeWarpRange(stat)}`
|
|
153
211
|
break
|
|
154
212
|
}
|
|
213
|
+
case MODULE_BATTERY: {
|
|
214
|
+
const vol = decodeStat(stats, 0)
|
|
215
|
+
const thm = decodeStat(stats, 1)
|
|
216
|
+
const pla = decodeStat(stats, 2)
|
|
217
|
+
const ins = decodeStat(stats, 3)
|
|
218
|
+
out += ` Energy Capacity ${computeBatteryBankCapacity(vol, thm, pla, ins)}`
|
|
219
|
+
break
|
|
220
|
+
}
|
|
155
221
|
}
|
|
156
222
|
return out
|
|
157
223
|
}
|
|
@@ -168,6 +234,14 @@ export function buildEntityDescription(
|
|
|
168
234
|
baseCapacity = computeBaseCapacityShip(hullStats)
|
|
169
235
|
} else if (itemId === ITEM_WAREHOUSE_T1_PACKED) {
|
|
170
236
|
baseCapacity = computeBaseCapacityWarehouse(hullStats)
|
|
237
|
+
} else if (
|
|
238
|
+
itemId === ITEM_EXTRACTOR_T1_PACKED ||
|
|
239
|
+
itemId === ITEM_FACTORY_T1_PACKED ||
|
|
240
|
+
itemId === ITEM_CONTAINER_T1_PACKED
|
|
241
|
+
) {
|
|
242
|
+
baseCapacity = computeBaseCapacityContainer(hullStats)
|
|
243
|
+
} else if (itemId === ITEM_CONTAINER_T2_PACKED) {
|
|
244
|
+
baseCapacity = computeBaseCapacityContainerT2(hullStats)
|
|
171
245
|
}
|
|
172
246
|
|
|
173
247
|
let out = entityDisplayName(itemId)
|
package/src/nft/index.ts
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type {GathererStats} from '../types/capabilities'
|
|
2
|
+
import type {ServerContract} from '../contracts'
|
|
3
|
+
import {
|
|
4
|
+
calc_gather_duration,
|
|
5
|
+
calc_gather_energy,
|
|
6
|
+
GATHER_MASS_DIVISOR,
|
|
7
|
+
} from '../capabilities/gathering'
|
|
8
|
+
import {projectRemainingAt, type Projectable} from '../scheduling/projection'
|
|
9
|
+
|
|
10
|
+
export interface LanePlanEntry {
|
|
11
|
+
slot: number
|
|
12
|
+
quantity: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PlanTarget = {quantity: number} | 'max'
|
|
16
|
+
|
|
17
|
+
export interface GatherPlanEntity extends Projectable {
|
|
18
|
+
gatherer_lanes: ServerContract.Types.gatherer_lane[]
|
|
19
|
+
loader_lanes: ServerContract.Types.loader_lane[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// massFactor=1 so plan-time cost is conservative; on-chain recomputes with real mass/richness.
|
|
23
|
+
const PLAN_ITEM_MASS = GATHER_MASS_DIVISOR
|
|
24
|
+
const PLAN_RICHNESS = 1000
|
|
25
|
+
|
|
26
|
+
// 'max' ceiling; Phase 3 passes a real reserve/capacity cap instead.
|
|
27
|
+
const MAX_PLAN_QTY = 10000
|
|
28
|
+
|
|
29
|
+
function gatherEnergyCost(
|
|
30
|
+
lane: ServerContract.Types.gatherer_lane,
|
|
31
|
+
quantity: number,
|
|
32
|
+
stratum: number
|
|
33
|
+
): number {
|
|
34
|
+
const stats = lane as unknown as GathererStats
|
|
35
|
+
const dur = Number(
|
|
36
|
+
calc_gather_duration(stats, PLAN_ITEM_MASS, quantity, stratum, PLAN_RICHNESS)
|
|
37
|
+
)
|
|
38
|
+
return Number(calc_gather_energy(stats, dur))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function allocateProportional(
|
|
42
|
+
lanes: {slot: number; weight: number}[],
|
|
43
|
+
total: number
|
|
44
|
+
): LanePlanEntry[] {
|
|
45
|
+
if (lanes.length === 0) return []
|
|
46
|
+
const weightSum = lanes.reduce((s, l) => s + l.weight, 0)
|
|
47
|
+
if (weightSum === 0) return []
|
|
48
|
+
|
|
49
|
+
const entries: LanePlanEntry[] = lanes.map((l) => ({
|
|
50
|
+
slot: l.slot,
|
|
51
|
+
quantity: Math.floor((total * l.weight) / weightSum),
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
let remainder = total - entries.reduce((s, e) => s + e.quantity, 0)
|
|
55
|
+
for (let i = 0; remainder > 0; i = (i + 1) % entries.length) {
|
|
56
|
+
entries[i].quantity++
|
|
57
|
+
remainder--
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return entries
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function planParallelGather(
|
|
64
|
+
entity: GatherPlanEntity,
|
|
65
|
+
target: PlanTarget,
|
|
66
|
+
stratum: number,
|
|
67
|
+
now: Date
|
|
68
|
+
): LanePlanEntry[] {
|
|
69
|
+
const reaching = entity.gatherer_lanes.filter((l) => l.depth.toNumber() >= stratum)
|
|
70
|
+
if (reaching.length === 0) throw new Error('no gatherer reaches this stratum')
|
|
71
|
+
|
|
72
|
+
// Projected energy nets out already-queued/in-flight task costs (contract projected_energy()).
|
|
73
|
+
const energy = entity.generator ? Number(projectRemainingAt(entity, now).energy) : Infinity
|
|
74
|
+
|
|
75
|
+
const requestedQty = target === 'max' ? MAX_PLAN_QTY : (target as {quantity: number}).quantity
|
|
76
|
+
|
|
77
|
+
// Ascending by yield so slice(1) sheds the lowest-yield lane first when energy-starved.
|
|
78
|
+
let activeLanes = reaching.slice().sort((a, b) => a.yield.toNumber() - b.yield.toNumber())
|
|
79
|
+
|
|
80
|
+
while (activeLanes.length > 0) {
|
|
81
|
+
const laneWeights = activeLanes.map((l) => ({
|
|
82
|
+
slot: l.slot_index.toNumber(),
|
|
83
|
+
weight: l.yield.toNumber(),
|
|
84
|
+
}))
|
|
85
|
+
|
|
86
|
+
const proposed = allocateProportional(laneWeights, requestedQty)
|
|
87
|
+
|
|
88
|
+
const totalEnergyCost = proposed.reduce((sum, entry) => {
|
|
89
|
+
const lane = activeLanes.find((l) => l.slot_index.toNumber() === entry.slot)!
|
|
90
|
+
return sum + gatherEnergyCost(lane, entry.quantity, stratum)
|
|
91
|
+
}, 0)
|
|
92
|
+
|
|
93
|
+
if (totalEnergyCost <= energy) {
|
|
94
|
+
return proposed.filter((e) => e.quantity > 0)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (activeLanes.length === 1) {
|
|
98
|
+
const lane = activeLanes[0]
|
|
99
|
+
const energyPerUnit = gatherEnergyCost(lane, 1, stratum)
|
|
100
|
+
if (energyPerUnit === 0) return proposed.filter((e) => e.quantity > 0)
|
|
101
|
+
const maxQty = Math.min(requestedQty, Math.floor(energy / energyPerUnit))
|
|
102
|
+
if (maxQty <= 0) return []
|
|
103
|
+
return [{slot: lane.slot_index.toNumber(), quantity: maxQty}]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
activeLanes = activeLanes.slice(1)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return []
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function planParallelTransfer(
|
|
113
|
+
entity: GatherPlanEntity,
|
|
114
|
+
target: PlanTarget
|
|
115
|
+
): LanePlanEntry[] {
|
|
116
|
+
const lanes = entity.loader_lanes.filter((l) => l.thrust.toNumber() > 0)
|
|
117
|
+
if (lanes.length === 0) return []
|
|
118
|
+
|
|
119
|
+
const requestedQty = target === 'max' ? MAX_PLAN_QTY : (target as {quantity: number}).quantity
|
|
120
|
+
|
|
121
|
+
const laneWeights = lanes.map((l) => ({
|
|
122
|
+
slot: l.slot_index.toNumber(),
|
|
123
|
+
weight: l.thrust.toNumber(),
|
|
124
|
+
}))
|
|
125
|
+
|
|
126
|
+
return allocateProportional(laneWeights, requestedQty).filter((e) => e.quantity > 0)
|
|
127
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import {UInt8, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
|
|
2
|
+
import {expect, test, describe} from 'bun:test'
|
|
3
|
+
import type {GathererStats} from '../types/capabilities'
|
|
4
|
+
import {ServerContract} from '../contracts'
|
|
5
|
+
import {calc_gather_duration} from '../capabilities/gathering'
|
|
6
|
+
import {planParallelGather, planParallelTransfer, type GatherPlanEntity} from './index'
|
|
7
|
+
|
|
8
|
+
function gathererLane(
|
|
9
|
+
slotIndex: number,
|
|
10
|
+
yieldVal: number,
|
|
11
|
+
drain: number,
|
|
12
|
+
depth: number
|
|
13
|
+
): ServerContract.Types.gatherer_lane {
|
|
14
|
+
return ServerContract.Types.gatherer_lane.from({
|
|
15
|
+
slot_index: UInt8.from(slotIndex),
|
|
16
|
+
yield: UInt16.from(yieldVal),
|
|
17
|
+
drain: UInt32.from(drain),
|
|
18
|
+
depth: UInt16.from(depth),
|
|
19
|
+
output_pct: UInt16.from(100),
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loaderLane(
|
|
24
|
+
slotIndex: number,
|
|
25
|
+
mass: number,
|
|
26
|
+
thrust: number
|
|
27
|
+
): ServerContract.Types.loader_lane {
|
|
28
|
+
return ServerContract.Types.loader_lane.from({
|
|
29
|
+
slot_index: UInt8.from(slotIndex),
|
|
30
|
+
mass: UInt32.from(mass),
|
|
31
|
+
thrust: UInt16.from(thrust),
|
|
32
|
+
output_pct: UInt16.from(100),
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function energyStats(capacity: number, recharge: number): ServerContract.Types.energy_stats {
|
|
37
|
+
return ServerContract.Types.energy_stats.from({
|
|
38
|
+
capacity: UInt32.from(capacity),
|
|
39
|
+
recharge: UInt32.from(recharge),
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface EntityOverrides {
|
|
44
|
+
gatherer_lanes?: ServerContract.Types.gatherer_lane[]
|
|
45
|
+
loader_lanes?: ServerContract.Types.loader_lane[]
|
|
46
|
+
generator?: ServerContract.Types.energy_stats
|
|
47
|
+
energy?: number
|
|
48
|
+
lanes?: ServerContract.Types.lane[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function entity(overrides: EntityOverrides = {}): GatherPlanEntity {
|
|
52
|
+
return {
|
|
53
|
+
gatherer_lanes: overrides.gatherer_lanes ?? [],
|
|
54
|
+
loader_lanes: overrides.loader_lanes ?? [],
|
|
55
|
+
generator: overrides.generator,
|
|
56
|
+
energy: overrides.energy !== undefined ? UInt16.from(overrides.energy) : undefined,
|
|
57
|
+
lanes: overrides.lanes ?? [],
|
|
58
|
+
coordinates: ServerContract.Types.coordinates.from({x: 0, y: 0}),
|
|
59
|
+
cargo: [],
|
|
60
|
+
cargomass: UInt32.from(0),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const NOW = new Date('2026-06-21T00:00:00.000Z')
|
|
65
|
+
|
|
66
|
+
describe('planParallelGather', () => {
|
|
67
|
+
test('sanity: single-gatherer qty 20 = ~35s matches calc_gather_duration', () => {
|
|
68
|
+
const gatherer: GathererStats = {
|
|
69
|
+
yield: UInt16.from(57),
|
|
70
|
+
drain: UInt32.from(500),
|
|
71
|
+
depth: UInt16.from(5000),
|
|
72
|
+
}
|
|
73
|
+
const dur = calc_gather_duration(gatherer, 228, 20, 0, 1000)
|
|
74
|
+
expect(Number(dur)).toBeCloseTo(35, 0)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('two gatherers: quantities proportional to yield, durations within 1s', () => {
|
|
78
|
+
const YIELD1 = 200
|
|
79
|
+
const YIELD2 = 400
|
|
80
|
+
const DEPTH = 5000
|
|
81
|
+
const DRAIN = 500
|
|
82
|
+
const QUANTITY = 60
|
|
83
|
+
const STRATUM = 0
|
|
84
|
+
|
|
85
|
+
const e = entity({
|
|
86
|
+
gatherer_lanes: [
|
|
87
|
+
gathererLane(0, YIELD1, DRAIN, DEPTH),
|
|
88
|
+
gathererLane(1, YIELD2, DRAIN, DEPTH),
|
|
89
|
+
],
|
|
90
|
+
generator: energyStats(10000, 100),
|
|
91
|
+
energy: 10000,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const plan = planParallelGather(e, {quantity: QUANTITY}, STRATUM, NOW)
|
|
95
|
+
|
|
96
|
+
expect(plan).toHaveLength(2)
|
|
97
|
+
expect(plan.reduce((s, p) => s + p.quantity, 0)).toBe(QUANTITY)
|
|
98
|
+
|
|
99
|
+
const q1 = plan.find((p) => p.slot === 0)!.quantity
|
|
100
|
+
const q2 = plan.find((p) => p.slot === 1)!.quantity
|
|
101
|
+
expect(q1 + q2).toBe(QUANTITY)
|
|
102
|
+
expect(q2 / q1).toBeCloseTo(YIELD2 / YIELD1, 0)
|
|
103
|
+
|
|
104
|
+
const ITEM_MASS = 228
|
|
105
|
+
const RICHNESS = 1000
|
|
106
|
+
const g1: GathererStats = {
|
|
107
|
+
yield: UInt16.from(YIELD1),
|
|
108
|
+
drain: UInt32.from(DRAIN),
|
|
109
|
+
depth: UInt16.from(DEPTH),
|
|
110
|
+
}
|
|
111
|
+
const g2: GathererStats = {
|
|
112
|
+
yield: UInt16.from(YIELD2),
|
|
113
|
+
drain: UInt32.from(DRAIN),
|
|
114
|
+
depth: UInt16.from(DEPTH),
|
|
115
|
+
}
|
|
116
|
+
const dur1 = Number(calc_gather_duration(g1, ITEM_MASS, q1, STRATUM, RICHNESS))
|
|
117
|
+
const dur2 = Number(calc_gather_duration(g2, ITEM_MASS, q2, STRATUM, RICHNESS))
|
|
118
|
+
expect(Math.abs(dur1 - dur2)).toBeLessThan(1)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test("'max' target: uses all reaching lanes, each slot gets >= 1 unit", () => {
|
|
122
|
+
const e = entity({
|
|
123
|
+
gatherer_lanes: [gathererLane(0, 200, 500, 5000), gathererLane(1, 300, 500, 5000)],
|
|
124
|
+
generator: energyStats(10000, 100),
|
|
125
|
+
energy: 10000,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const plan = planParallelGather(e, 'max', 0, NOW)
|
|
129
|
+
|
|
130
|
+
expect(plan.length).toBeGreaterThan(0)
|
|
131
|
+
for (const entry of plan) {
|
|
132
|
+
expect(entry.quantity).toBeGreaterThanOrEqual(1)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('energy-starved: drops the lowest-yield lane(s) until the pool sustains the plan', () => {
|
|
137
|
+
// Full-Q energy: 3 lanes=>144, 2 lanes(drop yield=50)=>120; pool 130 fits 2 but not 3.
|
|
138
|
+
const e = entity({
|
|
139
|
+
gatherer_lanes: [
|
|
140
|
+
gathererLane(0, 50, 10000, 5000),
|
|
141
|
+
gathererLane(1, 100, 10000, 5000),
|
|
142
|
+
gathererLane(2, 100, 10000, 5000),
|
|
143
|
+
],
|
|
144
|
+
generator: energyStats(10000, 1),
|
|
145
|
+
energy: 130,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const plan = planParallelGather(e, {quantity: 120}, 0, NOW)
|
|
149
|
+
|
|
150
|
+
// Lowest-yield lane (slot 0) dropped; the two yield-100 lanes survive.
|
|
151
|
+
expect(plan).toHaveLength(2)
|
|
152
|
+
expect(plan.find((p) => p.slot === 0)).toBeUndefined()
|
|
153
|
+
expect(plan.reduce((s, p) => s + p.quantity, 0)).toBe(120)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('energy-starved: single lane caps quantity to the sustainable max', () => {
|
|
157
|
+
// energyPerUnit=1, full Q=120 costs 120 > pool 50, so quantity caps at 50.
|
|
158
|
+
const e = entity({
|
|
159
|
+
gatherer_lanes: [gathererLane(0, 100, 10000, 5000)],
|
|
160
|
+
generator: energyStats(10000, 1),
|
|
161
|
+
energy: 50,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const plan = planParallelGather(e, {quantity: 120}, 0, NOW)
|
|
165
|
+
|
|
166
|
+
expect(plan).toHaveLength(1)
|
|
167
|
+
expect(plan[0].slot).toBe(0)
|
|
168
|
+
expect(plan[0].quantity).toBe(50)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('energy-starved: projected energy nets out a queued gather task', () => {
|
|
172
|
+
// A queued task costing 9970 leaves 30 projected energy => quantity caps at 30.
|
|
173
|
+
const queued = ServerContract.Types.lane.from({
|
|
174
|
+
lane_key: UInt8.from(0),
|
|
175
|
+
schedule: {
|
|
176
|
+
started: NOW.toISOString().slice(0, -1),
|
|
177
|
+
tasks: [
|
|
178
|
+
ServerContract.Types.task.from({
|
|
179
|
+
type: UInt8.from(5),
|
|
180
|
+
duration: UInt32.from(100),
|
|
181
|
+
cancelable: 0,
|
|
182
|
+
cargo: [],
|
|
183
|
+
entitytarget: {entity_type: 'ship', entity_id: UInt64.from(1)},
|
|
184
|
+
energy_cost: UInt32.from(9970),
|
|
185
|
+
}),
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const e = entity({
|
|
191
|
+
gatherer_lanes: [gathererLane(0, 100, 10000, 5000)],
|
|
192
|
+
generator: energyStats(10000, 1),
|
|
193
|
+
energy: 10000,
|
|
194
|
+
lanes: [queued],
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const plan = planParallelGather(e, {quantity: 120}, 0, NOW)
|
|
198
|
+
|
|
199
|
+
expect(plan).toHaveLength(1)
|
|
200
|
+
expect(plan[0].quantity).toBe(30)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('stratum filter: shallow lane excluded, only deep lane used', () => {
|
|
204
|
+
const e = entity({
|
|
205
|
+
gatherer_lanes: [gathererLane(0, 200, 500, 500), gathererLane(1, 300, 500, 5000)],
|
|
206
|
+
generator: energyStats(10000, 100),
|
|
207
|
+
energy: 10000,
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const plan = planParallelGather(e, {quantity: 10}, 2000, NOW)
|
|
211
|
+
|
|
212
|
+
expect(plan).toHaveLength(1)
|
|
213
|
+
expect(plan[0].slot).toBe(1)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('no reaching gatherers throws', () => {
|
|
217
|
+
const e = entity({
|
|
218
|
+
gatherer_lanes: [gathererLane(0, 200, 500, 100)],
|
|
219
|
+
generator: energyStats(10000, 100),
|
|
220
|
+
energy: 10000,
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
expect(() => planParallelGather(e, {quantity: 10}, 2000, NOW)).toThrow(
|
|
224
|
+
'no gatherer reaches this stratum'
|
|
225
|
+
)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('two identical gatherers: per-lane quantity halved, durations equal and within 1s', () => {
|
|
229
|
+
const YIELD = 200
|
|
230
|
+
const DEPTH = 5000
|
|
231
|
+
const DRAIN = 500
|
|
232
|
+
const QUANTITY = 60
|
|
233
|
+
const STRATUM = 0
|
|
234
|
+
|
|
235
|
+
const eSingle = entity({
|
|
236
|
+
gatherer_lanes: [gathererLane(0, YIELD, DRAIN, DEPTH)],
|
|
237
|
+
generator: energyStats(10000, 100),
|
|
238
|
+
energy: 10000,
|
|
239
|
+
})
|
|
240
|
+
const eDouble = entity({
|
|
241
|
+
gatherer_lanes: [
|
|
242
|
+
gathererLane(0, YIELD, DRAIN, DEPTH),
|
|
243
|
+
gathererLane(1, YIELD, DRAIN, DEPTH),
|
|
244
|
+
],
|
|
245
|
+
generator: energyStats(10000, 100),
|
|
246
|
+
energy: 10000,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const planSingle = planParallelGather(eSingle, {quantity: QUANTITY}, STRATUM, NOW)
|
|
250
|
+
const planDouble = planParallelGather(eDouble, {quantity: QUANTITY}, STRATUM, NOW)
|
|
251
|
+
|
|
252
|
+
expect(planSingle).toHaveLength(1)
|
|
253
|
+
expect(planDouble).toHaveLength(2)
|
|
254
|
+
|
|
255
|
+
const singleQ = planSingle[0].quantity
|
|
256
|
+
const doubleQ1 = planDouble[0].quantity
|
|
257
|
+
const doubleQ2 = planDouble[1].quantity
|
|
258
|
+
expect(doubleQ1 + doubleQ2).toBe(QUANTITY)
|
|
259
|
+
|
|
260
|
+
const ITEM_MASS = 228
|
|
261
|
+
const RICHNESS = 1000
|
|
262
|
+
const g: GathererStats = {
|
|
263
|
+
yield: UInt16.from(YIELD),
|
|
264
|
+
drain: UInt32.from(DRAIN),
|
|
265
|
+
depth: UInt16.from(DEPTH),
|
|
266
|
+
}
|
|
267
|
+
const durSingle = Number(calc_gather_duration(g, ITEM_MASS, singleQ, STRATUM, RICHNESS))
|
|
268
|
+
const durDouble1 = Number(calc_gather_duration(g, ITEM_MASS, doubleQ1, STRATUM, RICHNESS))
|
|
269
|
+
const durDouble2 = Number(calc_gather_duration(g, ITEM_MASS, doubleQ2, STRATUM, RICHNESS))
|
|
270
|
+
|
|
271
|
+
expect(durDouble1).toBeCloseTo(durSingle / 2, 0)
|
|
272
|
+
expect(durDouble2).toBeCloseTo(durSingle / 2, 0)
|
|
273
|
+
expect(Math.abs(durDouble1 - durDouble2)).toBeLessThan(1)
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
describe('planParallelTransfer', () => {
|
|
278
|
+
test('two loader lanes: quantities proportional to thrust, sums to target', () => {
|
|
279
|
+
const THRUST1 = 100
|
|
280
|
+
const THRUST2 = 200
|
|
281
|
+
const QUANTITY = 90
|
|
282
|
+
|
|
283
|
+
const e = entity({
|
|
284
|
+
loader_lanes: [loaderLane(0, 500, THRUST1), loaderLane(1, 500, THRUST2)],
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
const plan = planParallelTransfer(e, {quantity: QUANTITY})
|
|
288
|
+
|
|
289
|
+
expect(plan).toHaveLength(2)
|
|
290
|
+
expect(plan.reduce((s, p) => s + p.quantity, 0)).toBe(QUANTITY)
|
|
291
|
+
|
|
292
|
+
const q1 = plan.find((p) => p.slot === 0)!.quantity
|
|
293
|
+
const q2 = plan.find((p) => p.slot === 1)!.quantity
|
|
294
|
+
expect(q2 / q1).toBeCloseTo(THRUST2 / THRUST1, 0)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test('no loader lanes: returns empty plan', () => {
|
|
298
|
+
const plan = planParallelTransfer(entity({loader_lanes: []}), {quantity: 10})
|
|
299
|
+
expect(plan).toHaveLength(0)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('thrust=0 loader lane (no-loader/mobility case): returns empty plan', () => {
|
|
303
|
+
const plan = planParallelTransfer(entity({loader_lanes: [loaderLane(0, 500, 0)]}), {
|
|
304
|
+
quantity: 10,
|
|
305
|
+
})
|
|
306
|
+
expect(plan).toHaveLength(0)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test("'max' target: each loader lane gets >= 1 unit", () => {
|
|
310
|
+
const e = entity({
|
|
311
|
+
loader_lanes: [loaderLane(0, 500, 100), loaderLane(1, 500, 200)],
|
|
312
|
+
})
|
|
313
|
+
const plan = planParallelTransfer(e, 'max')
|
|
314
|
+
expect(plan.length).toBeGreaterThan(0)
|
|
315
|
+
for (const entry of plan) {
|
|
316
|
+
expect(entry.quantity).toBeGreaterThanOrEqual(1)
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
})
|