@shipload/sdk 1.0.0-next.59 → 1.0.0-next.60
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 +66 -27
- package/lib/shipload.js +833 -336
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +822 -337
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +12 -0
- package/lib/testing.js +110 -1
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +110 -1
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/capabilities/crafting.test.ts +30 -2
- package/src/capabilities/crafting.ts +19 -0
- package/src/capabilities/modules.ts +4 -0
- package/src/contracts/server.ts +37 -1
- package/src/data/item-ids.ts +6 -0
- package/src/data/items.json +42 -0
- package/src/data/metadata.ts +35 -0
- package/src/data/recipes.json +246 -0
- package/src/derivation/capabilities.test.ts +106 -8
- package/src/derivation/capabilities.ts +101 -43
- package/src/derivation/wormhole.ts +17 -4
- package/src/index-module.ts +7 -1
- package/src/managers/actions.ts +23 -0
- package/src/nft/buildImmutableData.ts +12 -9
- package/src/nft/description.ts +52 -17
- package/src/resolution/resolve-item.ts +5 -5
- package/src/scheduling/availability.test.ts +47 -0
- package/src/scheduling/availability.ts +24 -3
- package/src/scheduling/cluster-stock.test.ts +31 -0
- package/src/scheduling/cluster-stock.ts +15 -0
- package/src/scheduling/lanes.test.ts +10 -9
- package/src/scheduling/lanes.ts +6 -6
- package/src/scheduling/projection.ts +7 -3
- package/src/scheduling/task-cargo.test.ts +44 -0
- package/src/scheduling/task-cargo.ts +1 -1
- package/src/types.ts +1 -0
|
@@ -76,10 +76,23 @@ function dist(a: {x: number; y: number}, b: {x: number; y: number}): number {
|
|
|
76
76
|
function inBounds(c: {x: number; y: number}): boolean {
|
|
77
77
|
return c.x >= COORD_MIN && c.x <= COORD_MAX && c.y >= COORD_MIN && c.y <= COORD_MAX
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
type RegionWormhole = {A: {x: number; y: number}; B: {x: number; y: number}} | null
|
|
80
|
+
|
|
81
|
+
// ~11 sha512 rolls per region; pure in (seed, region), so memoized for A* neighbor probes.
|
|
82
|
+
const regionCache = new Map<string, RegionWormhole>()
|
|
83
|
+
const REGION_CACHE_MAX = 16384
|
|
84
|
+
|
|
85
|
+
function wormholeOfRegion(seed: Checksum256Type, R: Region): RegionWormhole {
|
|
86
|
+
const cacheKey = `${seed}:${R.rx}:${R.ry}`
|
|
87
|
+
const cached = regionCache.get(cacheKey)
|
|
88
|
+
if (cached !== undefined) return cached
|
|
89
|
+
const result = deriveWormholeOfRegion(seed, R)
|
|
90
|
+
if (regionCache.size >= REGION_CACHE_MAX) regionCache.clear()
|
|
91
|
+
regionCache.set(cacheKey, result)
|
|
92
|
+
return result
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function deriveWormholeOfRegion(seed: Checksum256Type, R: Region): RegionWormhole {
|
|
83
96
|
const P = partnerRegion(seed, R)
|
|
84
97
|
if (P.rx === R.rx && P.ry === R.ry) return null
|
|
85
98
|
const key = pairKey(R, P)
|
package/src/index-module.ts
CHANGED
|
@@ -322,9 +322,15 @@ export {
|
|
|
322
322
|
calcCounterpartDelivery,
|
|
323
323
|
cargoKey,
|
|
324
324
|
cargoInputKey,
|
|
325
|
+
hasSourceCoupling,
|
|
326
|
+
hasIncomingCoupling,
|
|
325
327
|
} from './scheduling/availability'
|
|
326
328
|
export type {CargoInput, IncomingSource} from './scheduling/availability'
|
|
327
329
|
|
|
330
|
+
export {clusterStockAvailable} from './scheduling/cluster-stock'
|
|
331
|
+
|
|
332
|
+
export {calcClusterIntake, calcClustercraftDuration, INTAKE_RATE} from './capabilities/crafting'
|
|
333
|
+
|
|
328
334
|
export {maxCraftable} from './capabilities/craftable'
|
|
329
335
|
|
|
330
336
|
export {energyAtTime} from './scheduling/energy'
|
|
@@ -463,6 +469,7 @@ export {
|
|
|
463
469
|
GATHERER_DEPTH_TABLE,
|
|
464
470
|
GATHERER_DEPTH_MAX_TIER,
|
|
465
471
|
gathererDepthForTier,
|
|
472
|
+
computeGathererYield,
|
|
466
473
|
} from './derivation/capabilities'
|
|
467
474
|
export type {
|
|
468
475
|
GathererDepthParams,
|
|
@@ -577,7 +584,6 @@ export {
|
|
|
577
584
|
ENGINE_DRAIN_REF_THM,
|
|
578
585
|
computeGeneratorCap,
|
|
579
586
|
computeGeneratorRech,
|
|
580
|
-
computeGathererYield,
|
|
581
587
|
computeGathererDrain,
|
|
582
588
|
computeGathererDepth,
|
|
583
589
|
computeLoaderMass,
|
package/src/managers/actions.ts
CHANGED
|
@@ -472,6 +472,29 @@ export class ActionsManager extends BaseManager {
|
|
|
472
472
|
return this.server.action('craftjob', params)
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
clustercraft(
|
|
476
|
+
entityId: UInt64Type,
|
|
477
|
+
recipeId: number,
|
|
478
|
+
quantity: number,
|
|
479
|
+
sources: ServerContract.ActionParams.Type.cluster_source[],
|
|
480
|
+
target?: UInt64Type,
|
|
481
|
+
slot?: UInt8Type
|
|
482
|
+
): Action {
|
|
483
|
+
const params: ServerContract.ActionParams.clustercraft = {
|
|
484
|
+
id: UInt64.from(entityId),
|
|
485
|
+
recipe_id: UInt16.from(recipeId),
|
|
486
|
+
quantity: UInt32.from(quantity),
|
|
487
|
+
sources,
|
|
488
|
+
}
|
|
489
|
+
if (target !== undefined) {
|
|
490
|
+
params.target = UInt64.from(target)
|
|
491
|
+
}
|
|
492
|
+
if (slot !== undefined) {
|
|
493
|
+
params.slot = UInt8.from(slot)
|
|
494
|
+
}
|
|
495
|
+
return this.server.action('clustercraft', params)
|
|
496
|
+
}
|
|
497
|
+
|
|
475
498
|
claimjob(jobId: UInt64Type, shipId: UInt64Type): Action {
|
|
476
499
|
const params: ServerContract.ActionParams.claimjob = {
|
|
477
500
|
job_id: UInt64.from(jobId),
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
MODULE_WARP,
|
|
15
15
|
} from '../capabilities/modules'
|
|
16
16
|
import {decodeStat, decodeCraftedItemStats} from '../derivation/crafting'
|
|
17
|
+
import {computeGathererYield} from '../derivation/capabilities'
|
|
17
18
|
import {computeEffectiveModuleStat} from '../derivation/stat-scaling'
|
|
18
19
|
import {getStatDefinitions} from '../derivation/stats'
|
|
19
20
|
import type {ResourceCategory} from '../types'
|
|
@@ -28,7 +29,6 @@ import {
|
|
|
28
29
|
computeEngineThrust,
|
|
29
30
|
computeGathererDepth,
|
|
30
31
|
computeGathererDrain,
|
|
31
|
-
computeGathererYield,
|
|
32
32
|
computeGeneratorCap,
|
|
33
33
|
computeGeneratorRech,
|
|
34
34
|
computeCargoBayCapacity,
|
|
@@ -194,7 +194,7 @@ export function buildModuleImmutable(
|
|
|
194
194
|
base.push({first: 'thermal', second: ['uint16', thm]})
|
|
195
195
|
base.push({
|
|
196
196
|
first: 'thrust',
|
|
197
|
-
second: ['uint32', computeEngineThrust(computeEffectiveModuleStat(vol))],
|
|
197
|
+
second: ['uint32', computeEngineThrust(computeEffectiveModuleStat(vol), item.tier)],
|
|
198
198
|
})
|
|
199
199
|
base.push({
|
|
200
200
|
first: 'drain',
|
|
@@ -211,14 +211,14 @@ export function buildModuleImmutable(
|
|
|
211
211
|
first: 'capacity',
|
|
212
212
|
second: [
|
|
213
213
|
'uint16',
|
|
214
|
-
toWholeEnergy(computeGeneratorCap(computeEffectiveModuleStat(res))),
|
|
214
|
+
toWholeEnergy(computeGeneratorCap(computeEffectiveModuleStat(res), item.tier)),
|
|
215
215
|
],
|
|
216
216
|
})
|
|
217
217
|
base.push({
|
|
218
218
|
first: 'recharge',
|
|
219
219
|
second: [
|
|
220
220
|
'uint16',
|
|
221
|
-
toWholeEnergy(computeGeneratorRech(computeEffectiveModuleStat(ref))),
|
|
221
|
+
toWholeEnergy(computeGeneratorRech(computeEffectiveModuleStat(ref), item.tier)),
|
|
222
222
|
],
|
|
223
223
|
})
|
|
224
224
|
break
|
|
@@ -230,7 +230,10 @@ export function buildModuleImmutable(
|
|
|
230
230
|
base.push({first: 'strength', second: ['uint16', str]})
|
|
231
231
|
base.push({first: 'hardness', second: ['uint16', hrd]})
|
|
232
232
|
base.push({first: 'saturation', second: ['uint16', sat]})
|
|
233
|
-
base.push({
|
|
233
|
+
base.push({
|
|
234
|
+
first: 'yield',
|
|
235
|
+
second: ['uint16', computeGathererYield(str, item.tier)],
|
|
236
|
+
})
|
|
234
237
|
base.push({
|
|
235
238
|
first: 'drain',
|
|
236
239
|
second: ['uint16', toWholeEnergy(computeGathererDrain(sat))],
|
|
@@ -244,13 +247,13 @@ export function buildModuleImmutable(
|
|
|
244
247
|
base.push({first: 'fineness', second: ['uint16', fin]})
|
|
245
248
|
base.push({first: 'plasticity', second: ['uint16', pla]})
|
|
246
249
|
base.push({first: 'mass', second: ['uint32', computeLoaderMass(fin)]})
|
|
247
|
-
base.push({first: 'thrust', second: ['uint16', computeLoaderThrust(pla)]})
|
|
250
|
+
base.push({first: 'thrust', second: ['uint16', computeLoaderThrust(pla, item.tier)]})
|
|
248
251
|
break
|
|
249
252
|
}
|
|
250
253
|
case MODULE_WARP: {
|
|
251
254
|
const ref = decodeStat(stats, 0)
|
|
252
255
|
base.push({first: 'reflectivity', second: ['uint16', ref]})
|
|
253
|
-
base.push({first: 'range', second: ['uint32', computeWarpRange(ref)]})
|
|
256
|
+
base.push({first: 'range', second: ['uint32', computeWarpRange(ref, item.tier)]})
|
|
254
257
|
break
|
|
255
258
|
}
|
|
256
259
|
case MODULE_CRAFTER: {
|
|
@@ -258,7 +261,7 @@ export function buildModuleImmutable(
|
|
|
258
261
|
const con = decodeStat(stats, 1)
|
|
259
262
|
base.push({first: 'fineness', second: ['uint16', fin]})
|
|
260
263
|
base.push({first: 'conductivity', second: ['uint16', con]})
|
|
261
|
-
base.push({first: 'speed', second: ['uint16', computeCrafterSpeed(fin)]})
|
|
264
|
+
base.push({first: 'speed', second: ['uint16', computeCrafterSpeed(fin, item.tier)]})
|
|
262
265
|
base.push({first: 'drain', second: ['uint16', toWholeEnergy(computeCrafterDrain(con))]})
|
|
263
266
|
break
|
|
264
267
|
}
|
|
@@ -267,7 +270,7 @@ export function buildModuleImmutable(
|
|
|
267
270
|
const fin = decodeStat(stats, 1)
|
|
268
271
|
base.push({first: 'resonance', second: ['uint16', res]})
|
|
269
272
|
base.push({first: 'fineness', second: ['uint16', fin]})
|
|
270
|
-
base.push({first: 'speed', second: ['uint16', computeBuilderSpeed(res)]})
|
|
273
|
+
base.push({first: 'speed', second: ['uint16', computeBuilderSpeed(res, item.tier)]})
|
|
271
274
|
base.push({first: 'drain', second: ['uint16', toWholeEnergy(computeBuilderDrain(fin))]})
|
|
272
275
|
break
|
|
273
276
|
}
|
package/src/nft/description.ts
CHANGED
|
@@ -16,18 +16,23 @@ import {
|
|
|
16
16
|
ITEM_CONTAINER_T2_PACKED,
|
|
17
17
|
ITEM_CONSTRUCTION_DOCK_T1_PACKED,
|
|
18
18
|
ITEM_BUILDER_T1,
|
|
19
|
+
ITEM_BUILDER_T2,
|
|
19
20
|
ITEM_CRAFTER_T1,
|
|
21
|
+
ITEM_CRAFTER_T2,
|
|
20
22
|
ITEM_ENGINE_T1,
|
|
23
|
+
ITEM_ENGINE_T2,
|
|
21
24
|
ITEM_EXTRACTOR_T1_PACKED,
|
|
22
25
|
ITEM_FACTORY_T1_PACKED,
|
|
23
26
|
ITEM_GATHERER_T1,
|
|
24
27
|
ITEM_GATHERER_T2,
|
|
25
28
|
ITEM_GENERATOR_T1,
|
|
29
|
+
ITEM_GENERATOR_T2,
|
|
26
30
|
ITEM_HAULER_T1,
|
|
27
31
|
ITEM_HAULER_T2,
|
|
28
32
|
ITEM_BATTERY_T1,
|
|
29
33
|
ITEM_LAUNCHER_T1,
|
|
30
34
|
ITEM_LOADER_T1,
|
|
35
|
+
ITEM_LOADER_T2,
|
|
31
36
|
ITEM_PROSPECTOR_T1A_PACKED,
|
|
32
37
|
ITEM_PROSPECTOR_T2A_PACKED,
|
|
33
38
|
ITEM_PROSPECTOR_T2B_PACKED,
|
|
@@ -42,9 +47,21 @@ import {
|
|
|
42
47
|
ITEM_DREDGER_T2A_PACKED,
|
|
43
48
|
ITEM_WAREHOUSE_T1_PACKED,
|
|
44
49
|
ITEM_WARP_T1,
|
|
50
|
+
ITEM_WARP_T2,
|
|
45
51
|
} from '../data/item-ids'
|
|
46
52
|
import {decodeStat} from '../derivation/crafting'
|
|
47
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
gathererDepthForTier,
|
|
55
|
+
computeGathererYield,
|
|
56
|
+
moduleTierPct,
|
|
57
|
+
ENGINE_THRUST_TIER_PCT,
|
|
58
|
+
GENERATOR_CAPACITY_TIER_PCT,
|
|
59
|
+
GENERATOR_RECHARGE_TIER_PCT,
|
|
60
|
+
CRAFTER_SPEED_TIER_PCT,
|
|
61
|
+
BUILDER_SPEED_TIER_PCT,
|
|
62
|
+
WARP_RANGE_TIER_PCT,
|
|
63
|
+
LOADER_THRUST_TIER_PCT,
|
|
64
|
+
} from '../derivation/capabilities'
|
|
48
65
|
import {getItem} from '../data/catalog'
|
|
49
66
|
import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
|
|
50
67
|
import {getBaseHullmassFor} from '../derivation/capabilities'
|
|
@@ -92,7 +109,8 @@ export function computeBaseCapacityWarehouse(stats: bigint): number {
|
|
|
92
109
|
return Math.floor(100_000_000 * 6 ** (s / 1998))
|
|
93
110
|
}
|
|
94
111
|
|
|
95
|
-
export const computeEngineThrust = (vol: number): number =>
|
|
112
|
+
export const computeEngineThrust = (vol: number, tier: number): number =>
|
|
113
|
+
idiv((400 + idiv(vol * 3, 4)) * moduleTierPct(ENGINE_THRUST_TIER_PCT, tier), 100)
|
|
96
114
|
export const computeEngineDrain = (thm: number): number => 2 * Math.max(30, 50 - idiv(thm, 70))
|
|
97
115
|
export const ENGINE_DRAIN_BASE = 156
|
|
98
116
|
export const ENGINE_DRAIN_REF_THRUST = 775
|
|
@@ -104,19 +122,23 @@ export const computeTravelDrain = (totalThrust: number, avgThm: number): number
|
|
|
104
122
|
const thrustFactor = Math.sqrt(ENGINE_DRAIN_REF_THRUST / totalThrust)
|
|
105
123
|
return Math.floor(ENGINE_DRAIN_BASE * 1000 * thermalFactor * thrustFactor)
|
|
106
124
|
}
|
|
107
|
-
export const computeGeneratorCap = (com: number): number =>
|
|
108
|
-
|
|
109
|
-
export const
|
|
125
|
+
export const computeGeneratorCap = (com: number, tier: number): number =>
|
|
126
|
+
idiv((1_300_000 + com * 500) * moduleTierPct(GENERATOR_CAPACITY_TIER_PCT, tier), 100)
|
|
127
|
+
export const computeGeneratorRech = (fin: number, tier: number): number =>
|
|
128
|
+
idiv((2000 + fin * 6) * moduleTierPct(GENERATOR_RECHARGE_TIER_PCT, tier), 100)
|
|
110
129
|
export const computeGathererDrain = (con: number): number =>
|
|
111
130
|
2 * Math.max(250_000, 1_250_000 - con * 1250)
|
|
112
131
|
export const computeGathererDepth = (tol: number, tier: number): number =>
|
|
113
132
|
gathererDepthForTier(tol, tier)
|
|
114
133
|
export const computeLoaderMass = (ins: number): number => Math.max(200, 2000 - ins * 2)
|
|
115
|
-
export const computeLoaderThrust = (pla: number): number =>
|
|
116
|
-
|
|
134
|
+
export const computeLoaderThrust = (pla: number, tier: number): number =>
|
|
135
|
+
idiv((1 + idiv(pla * pla, 10000)) * moduleTierPct(LOADER_THRUST_TIER_PCT, tier), 100)
|
|
136
|
+
export const computeCrafterSpeed = (rea: number, tier: number): number =>
|
|
137
|
+
idiv((100 + idiv(rea * 4, 5)) * moduleTierPct(CRAFTER_SPEED_TIER_PCT, tier), 100)
|
|
117
138
|
export const computeCrafterDrain = (fin: number): number =>
|
|
118
139
|
Math.max(5000, 30000 - idiv(fin * 1000, 33))
|
|
119
|
-
export const computeBuilderSpeed = (resonance: number): number =>
|
|
140
|
+
export const computeBuilderSpeed = (resonance: number, tier: number): number =>
|
|
141
|
+
idiv((100 + idiv(resonance * 4, 5)) * moduleTierPct(BUILDER_SPEED_TIER_PCT, tier), 100)
|
|
120
142
|
export const computeBuilderDrain = (fineness: number): number =>
|
|
121
143
|
Math.max(5000, 30000 - idiv(fineness * 1000, 33))
|
|
122
144
|
export const computeHaulerCapacity = (fin: number, tier: number): number =>
|
|
@@ -132,7 +154,8 @@ export const computeHaulerDrain = (conductivity: number, tier: number): number =
|
|
|
132
154
|
idiv(computeT1LogisticsDrain(conductivity) * supportDrainTierPercent(tier), 100)
|
|
133
155
|
export const computeCargoBayDrain = (cohesion: number, tier: number): number =>
|
|
134
156
|
idiv(computeT1LogisticsDrain(cohesion) * 3 * supportDrainTierPercent(tier), 400)
|
|
135
|
-
export const computeWarpRange = (stat: number): number =>
|
|
157
|
+
export const computeWarpRange = (stat: number, tier: number): number =>
|
|
158
|
+
idiv((100 + stat * 3) * moduleTierPct(WARP_RANGE_TIER_PCT, tier), 100)
|
|
136
159
|
export const computeCargoBayCapacity = (
|
|
137
160
|
strength: number,
|
|
138
161
|
density: number,
|
|
@@ -189,17 +212,22 @@ export function entityDisplayName(itemId: number): string {
|
|
|
189
212
|
export function moduleDisplayName(itemId: number): string {
|
|
190
213
|
switch (itemId) {
|
|
191
214
|
case ITEM_ENGINE_T1:
|
|
215
|
+
case ITEM_ENGINE_T2:
|
|
192
216
|
return 'Engine'
|
|
193
217
|
case ITEM_GENERATOR_T1:
|
|
218
|
+
case ITEM_GENERATOR_T2:
|
|
194
219
|
return 'Power Core'
|
|
195
220
|
case ITEM_GATHERER_T1:
|
|
196
221
|
case ITEM_GATHERER_T2:
|
|
197
222
|
return 'Limpet Bay'
|
|
198
223
|
case ITEM_LOADER_T1:
|
|
224
|
+
case ITEM_LOADER_T2:
|
|
199
225
|
return 'Shuttle Bay'
|
|
200
226
|
case ITEM_CRAFTER_T1:
|
|
227
|
+
case ITEM_CRAFTER_T2:
|
|
201
228
|
return 'Fabricator'
|
|
202
229
|
case ITEM_BUILDER_T1:
|
|
230
|
+
case ITEM_BUILDER_T2:
|
|
203
231
|
return 'Assembly Arm'
|
|
204
232
|
case ITEM_STORAGE_T1:
|
|
205
233
|
return 'Cargo Hold'
|
|
@@ -207,6 +235,7 @@ export function moduleDisplayName(itemId: number): string {
|
|
|
207
235
|
case ITEM_HAULER_T2:
|
|
208
236
|
return 'Tractor Beam'
|
|
209
237
|
case ITEM_WARP_T1:
|
|
238
|
+
case ITEM_WARP_T2:
|
|
210
239
|
return 'Warp Drive'
|
|
211
240
|
case ITEM_BATTERY_T1:
|
|
212
241
|
return 'Battery Bank'
|
|
@@ -231,14 +260,16 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
|
|
|
231
260
|
case MODULE_ENGINE: {
|
|
232
261
|
const vol = computeEffectiveModuleStat(decodeStat(stats, 0))
|
|
233
262
|
const thm = computeEffectiveModuleStat(decodeStat(stats, 1))
|
|
234
|
-
|
|
263
|
+
const tier = getItem(itemId).tier
|
|
264
|
+
out += ` Thrust ${computeEngineThrust(vol, tier)} Drain ${computeEngineDrain(thm)}`
|
|
235
265
|
break
|
|
236
266
|
}
|
|
237
267
|
case MODULE_GENERATOR: {
|
|
238
268
|
const res = computeEffectiveModuleStat(decodeStat(stats, 0))
|
|
239
269
|
const ref = computeEffectiveModuleStat(decodeStat(stats, 1))
|
|
240
|
-
|
|
241
|
-
|
|
270
|
+
const tier = getItem(itemId).tier
|
|
271
|
+
out += ` Capacity ${toWholeEnergy(computeGeneratorCap(res, tier))} Recharge ${toWholeEnergy(
|
|
272
|
+
computeGeneratorRech(ref, tier)
|
|
242
273
|
)}`
|
|
243
274
|
break
|
|
244
275
|
}
|
|
@@ -247,7 +278,7 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
|
|
|
247
278
|
const tol = decodeStat(stats, 1)
|
|
248
279
|
const con = decodeStat(stats, 2)
|
|
249
280
|
const tier = getItem(itemId).tier
|
|
250
|
-
out += ` Yield ${computeGathererYield(str)} Depth ${computeGathererDepth(
|
|
281
|
+
out += ` Yield ${computeGathererYield(str, tier)} Depth ${computeGathererDepth(
|
|
251
282
|
tol,
|
|
252
283
|
tier
|
|
253
284
|
)} Drain ${toWholeEnergy(computeGathererDrain(con))}`
|
|
@@ -256,19 +287,22 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
|
|
|
256
287
|
case MODULE_LOADER: {
|
|
257
288
|
const fin = decodeStat(stats, 0)
|
|
258
289
|
const pla = decodeStat(stats, 1)
|
|
259
|
-
|
|
290
|
+
const tier = getItem(itemId).tier
|
|
291
|
+
out += ` Mass ${computeLoaderMass(fin)} Thrust ${computeLoaderThrust(pla, tier)}`
|
|
260
292
|
break
|
|
261
293
|
}
|
|
262
294
|
case MODULE_CRAFTER: {
|
|
263
295
|
const rea = decodeStat(stats, 0)
|
|
264
296
|
const con = decodeStat(stats, 1)
|
|
265
|
-
|
|
297
|
+
const tier = getItem(itemId).tier
|
|
298
|
+
out += ` Speed ${computeCrafterSpeed(rea, tier)} Drain ${toWholeEnergy(computeCrafterDrain(con))}`
|
|
266
299
|
break
|
|
267
300
|
}
|
|
268
301
|
case MODULE_BUILDER: {
|
|
269
302
|
const res = decodeStat(stats, 0)
|
|
270
303
|
const fin = decodeStat(stats, 1)
|
|
271
|
-
|
|
304
|
+
const tier = getItem(itemId).tier
|
|
305
|
+
out += ` Speed ${computeBuilderSpeed(res, tier)} Drain ${toWholeEnergy(computeBuilderDrain(fin))}`
|
|
272
306
|
break
|
|
273
307
|
}
|
|
274
308
|
case MODULE_STORAGE: {
|
|
@@ -290,7 +324,8 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
|
|
|
290
324
|
}
|
|
291
325
|
case MODULE_WARP: {
|
|
292
326
|
const stat = decodeStat(stats, 0)
|
|
293
|
-
|
|
327
|
+
const tier = getItem(itemId).tier
|
|
328
|
+
out += ` Range ${computeWarpRange(stat, tier)}`
|
|
294
329
|
break
|
|
295
330
|
}
|
|
296
331
|
case MODULE_BATTERY: {
|
|
@@ -161,7 +161,7 @@ function computeCapabilityGroup(
|
|
|
161
161
|
): ResolvedAttributeGroup | undefined {
|
|
162
162
|
switch (moduleType) {
|
|
163
163
|
case MODULE_ENGINE: {
|
|
164
|
-
const caps = computeEngineCapabilities(stats)
|
|
164
|
+
const caps = computeEngineCapabilities(stats, tier)
|
|
165
165
|
return {
|
|
166
166
|
capability: 'Engine',
|
|
167
167
|
attributes: [
|
|
@@ -171,7 +171,7 @@ function computeCapabilityGroup(
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
case MODULE_GENERATOR: {
|
|
174
|
-
const caps = computeGeneratorCapabilities(stats)
|
|
174
|
+
const caps = computeGeneratorCapabilities(stats, tier)
|
|
175
175
|
return {
|
|
176
176
|
capability: 'Generator',
|
|
177
177
|
attributes: [
|
|
@@ -192,7 +192,7 @@ function computeCapabilityGroup(
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
case MODULE_LOADER: {
|
|
195
|
-
const caps = computeLoaderCapabilities(stats)
|
|
195
|
+
const caps = computeLoaderCapabilities(stats, tier)
|
|
196
196
|
return {
|
|
197
197
|
capability: 'Loading',
|
|
198
198
|
attributes: [
|
|
@@ -203,7 +203,7 @@ function computeCapabilityGroup(
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
case MODULE_CRAFTER: {
|
|
206
|
-
const caps = computeCrafterCapabilities(stats)
|
|
206
|
+
const caps = computeCrafterCapabilities(stats, tier)
|
|
207
207
|
return {
|
|
208
208
|
capability: 'Crafting',
|
|
209
209
|
attributes: [
|
|
@@ -213,7 +213,7 @@ function computeCapabilityGroup(
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
case MODULE_BUILDER: {
|
|
216
|
-
const caps = computeBuilderCapabilities(stats)
|
|
216
|
+
const caps = computeBuilderCapabilities(stats, tier)
|
|
217
217
|
return {
|
|
218
218
|
capability: 'Build',
|
|
219
219
|
attributes: [
|
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
calcCounterpartDelivery,
|
|
5
5
|
cargoReadyAt,
|
|
6
6
|
type CargoInput,
|
|
7
|
+
hasIncomingCoupling,
|
|
8
|
+
hasSourceCoupling,
|
|
7
9
|
type IncomingSource,
|
|
8
10
|
projectedCargoAvailableAt,
|
|
11
|
+
taskCargoEffect,
|
|
9
12
|
} from './availability'
|
|
10
13
|
|
|
11
14
|
const T0 = '2026-06-19T00:00:00'
|
|
@@ -33,6 +36,19 @@ function coupling(kind: number) {
|
|
|
33
36
|
})
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
function craftTask(
|
|
40
|
+
cargo: ReturnType<typeof cargoItem>[],
|
|
41
|
+
couplings: ReturnType<typeof coupling>[]
|
|
42
|
+
) {
|
|
43
|
+
return ServerContract.Types.task.from({
|
|
44
|
+
type: TaskType.CRAFT,
|
|
45
|
+
duration: 60,
|
|
46
|
+
cancelable: 0,
|
|
47
|
+
cargo,
|
|
48
|
+
couplings,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
function entity(
|
|
37
53
|
cargo: ReturnType<typeof cargoItem>[],
|
|
38
54
|
tasks: ReturnType<typeof task>[] = [],
|
|
@@ -155,3 +171,34 @@ describe('projectedCargoAvailableAt — incoming', () => {
|
|
|
155
171
|
expect(avail.get('7:0') ?? 0n).toBe(0n)
|
|
156
172
|
})
|
|
157
173
|
})
|
|
174
|
+
|
|
175
|
+
describe('clustercraft projection parity', () => {
|
|
176
|
+
const inputs = [cargoItem(1001, 0, 10), cargoItem(1002, 0, 5)]
|
|
177
|
+
const output = cargoItem(2001, 0, 1)
|
|
178
|
+
|
|
179
|
+
test('own-hold craft: inputs removed, output added', () => {
|
|
180
|
+
const eff = taskCargoEffect(craftTask([...inputs, output], []))
|
|
181
|
+
expect(eff.removed.length).toBe(2)
|
|
182
|
+
expect(eff.added).toEqual([output])
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('clustercraft to a target: no crafter debit, no crafter credit', () => {
|
|
186
|
+
const t = craftTask(
|
|
187
|
+
[...inputs, output],
|
|
188
|
+
[coupling(HoldKind.SOURCE), coupling(HoldKind.GATHER)]
|
|
189
|
+
)
|
|
190
|
+
expect(hasSourceCoupling(t)).toBe(true)
|
|
191
|
+
expect(hasIncomingCoupling(t)).toBe(true)
|
|
192
|
+
const eff = taskCargoEffect(t)
|
|
193
|
+
expect(eff.removed).toEqual([])
|
|
194
|
+
expect(eff.added).toEqual([])
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('clustercraft with target omitted: output falls back to crafter, still no input debit', () => {
|
|
198
|
+
const t = craftTask([...inputs, output], [coupling(HoldKind.SOURCE)])
|
|
199
|
+
expect(hasIncomingCoupling(t)).toBe(false)
|
|
200
|
+
const eff = taskCargoEffect(t)
|
|
201
|
+
expect(eff.removed).toEqual([])
|
|
202
|
+
expect(eff.added).toEqual([output])
|
|
203
|
+
})
|
|
204
|
+
})
|
|
@@ -36,6 +36,25 @@ export function isIncomingCouplingKind(kind: number): boolean {
|
|
|
36
36
|
return INCOMING_COUPLING_KINDS.has(kind)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Mirrors has_source_hold: a HOLD_SOURCE coupling marks an escrowed clustercraft.
|
|
40
|
+
export function hasSourceCoupling(task: Task): boolean {
|
|
41
|
+
return task.couplings.some((c) => c.kind.toNumber() === HoldKind.SOURCE)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Mirrors has_incoming_coupling: any PUSH/GATHER/FLIGHT coupling delivers output to a counterpart.
|
|
45
|
+
export function hasIncomingCoupling(task: Task): boolean {
|
|
46
|
+
return task.couplings.some((c) => isIncomingCouplingKind(c.kind.toNumber()))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A CRAFT task's output ownership: clustered escrows inputs off other members; own-output lands on the crafter.
|
|
50
|
+
export function craftCargoOwnership(task: Task): {clustered: boolean; ownOutput: boolean} {
|
|
51
|
+
const clustered = hasSourceCoupling(task)
|
|
52
|
+
return {
|
|
53
|
+
clustered,
|
|
54
|
+
ownOutput: clustered ? !hasIncomingCoupling(task) : task.couplings.length === 0,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
// Mirrors calc_counterpart_delivery: incoming-kind couplings only; a coupled CRAFT yields its output slot.
|
|
40
59
|
export function calcCounterpartDelivery(task: Task, coupling: Coupling): CargoItem[] {
|
|
41
60
|
if (!isIncomingCouplingKind(coupling.kind.toNumber())) return []
|
|
@@ -58,12 +77,14 @@ export function taskCargoEffect(task: Task): CargoEffect {
|
|
|
58
77
|
return task.couplings.length > 0
|
|
59
78
|
? {added: [], removed: []}
|
|
60
79
|
: {added: task.cargo, removed: []}
|
|
61
|
-
case TaskType.CRAFT:
|
|
80
|
+
case TaskType.CRAFT: {
|
|
62
81
|
if (task.cargo.length === 0) return {added: [], removed: []}
|
|
82
|
+
const {clustered, ownOutput} = craftCargoOwnership(task)
|
|
63
83
|
return {
|
|
64
|
-
added:
|
|
65
|
-
removed: task.cargo.slice(0, -1),
|
|
84
|
+
added: ownOutput ? [task.cargo[task.cargo.length - 1]] : [],
|
|
85
|
+
removed: clustered ? [] : task.cargo.slice(0, -1),
|
|
66
86
|
}
|
|
87
|
+
}
|
|
67
88
|
default:
|
|
68
89
|
return {added: [], removed: []}
|
|
69
90
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {ServerContract, TaskType} from '../index-module'
|
|
3
|
+
import {availableForItem} from './availability'
|
|
4
|
+
import {clusterStockAvailable} from './cluster-stock'
|
|
5
|
+
|
|
6
|
+
const AT = new Date('2026-06-19T00:00:00')
|
|
7
|
+
|
|
8
|
+
function cargoItem(itemId: number, stats: number, quantity: number) {
|
|
9
|
+
return ServerContract.Types.cargo_item.from({item_id: itemId, stats, modules: [], quantity})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function member(cargo: ReturnType<typeof cargoItem>[]) {
|
|
13
|
+
return {cargo, tasks: [], lanes: []} as unknown as Parameters<
|
|
14
|
+
typeof clusterStockAvailable
|
|
15
|
+
>[0][number]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('clusterStockAvailable', () => {
|
|
19
|
+
test('sums the same item split across two members', () => {
|
|
20
|
+
const stock = clusterStockAvailable(
|
|
21
|
+
[member([cargoItem(1001, 0, 30)]), member([cargoItem(1001, 0, 20)])],
|
|
22
|
+
AT
|
|
23
|
+
)
|
|
24
|
+
expect(availableForItem(stock, 1001)).toBe(50n)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('an empty cluster yields nothing', () => {
|
|
28
|
+
const stock = clusterStockAvailable([], AT)
|
|
29
|
+
expect(availableForItem(stock, 1001)).toBe(0n)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {type AvailabilityInput, projectedCargoAvailableAt} from './availability'
|
|
2
|
+
|
|
3
|
+
// Union of every cluster member's present available cargo (present-only; incoming/future not credited).
|
|
4
|
+
export function clusterStockAvailable(
|
|
5
|
+
members: readonly AvailabilityInput[],
|
|
6
|
+
at: Date
|
|
7
|
+
): Map<string, bigint> {
|
|
8
|
+
const total = new Map<string, bigint>()
|
|
9
|
+
for (const member of members) {
|
|
10
|
+
for (const [key, qty] of projectedCargoAvailableAt(member, at)) {
|
|
11
|
+
total.set(key, (total.get(key) ?? 0n) + qty)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return total
|
|
15
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {UInt8, UInt16, UInt64} from '@wharfkit/antelope'
|
|
2
2
|
import {expect, test} from 'bun:test'
|
|
3
3
|
import {encodeStats} from '../derivation/crafting'
|
|
4
|
-
import {
|
|
4
|
+
import {computeGathererDepth, computeGathererDrain} from '../nft/description'
|
|
5
|
+
import {computeGathererYield} from '../derivation/capabilities'
|
|
5
6
|
import {computeLoaderThrust, computeLoaderMass} from '../nft/description'
|
|
6
7
|
import {computeCrafterSpeed, computeCrafterDrain} from '../nft/description'
|
|
7
8
|
import {applySlotMultiplier} from '../entities/slot-multiplier'
|
|
@@ -77,8 +78,8 @@ test('resolveLaneGatherer reads the layout slot amp and applies it to yield (par
|
|
|
77
78
|
|
|
78
79
|
// The resolver routes through the real layout's amp, not a hardcoded 100.
|
|
79
80
|
expect(result.outputPct).toBe(ampFromLayout)
|
|
80
|
-
// Yield equals the contract formula clamp_to_uint16(compute_gatherer_yield(str) * amp / 100).
|
|
81
|
-
expect(result.yield).toBe(applySlotMultiplier(computeGathererYield(GATH_STR), ampFromLayout))
|
|
81
|
+
// Yield equals the contract formula clamp_to_uint16(compute_gatherer_yield(str, tier) * amp / 100).
|
|
82
|
+
expect(result.yield).toBe(applySlotMultiplier(computeGathererYield(GATH_STR, 1), ampFromLayout))
|
|
82
83
|
expect(result.drain).toBe(computeGathererDrain(GATH_CON))
|
|
83
84
|
expect(result.depth).toBe(computeGathererDepth(GATH_TOL, 1))
|
|
84
85
|
expect(result.slotIndex).toBe(GATHERER_SLOT_IDX)
|
|
@@ -86,11 +87,11 @@ test('resolveLaneGatherer reads the layout slot amp and applies it to yield (par
|
|
|
86
87
|
|
|
87
88
|
test('resolveLaneGatherer amp-scaling parity holds for a non-100 amp', () => {
|
|
88
89
|
// Parity for a non-100 amp: clamp_to_uint16(value * amp / 100).
|
|
89
|
-
expect(applySlotMultiplier(computeGathererYield(GATH_STR), 80)).toBe(
|
|
90
|
-
Math.min(Math.floor((computeGathererYield(GATH_STR) * 80) / 100), 65535)
|
|
90
|
+
expect(applySlotMultiplier(computeGathererYield(GATH_STR, 1), 80)).toBe(
|
|
91
|
+
Math.min(Math.floor((computeGathererYield(GATH_STR, 1) * 80) / 100), 65535)
|
|
91
92
|
)
|
|
92
|
-
expect(applySlotMultiplier(computeGathererYield(GATH_STR), 120)).toBe(
|
|
93
|
-
Math.floor((computeGathererYield(GATH_STR) * 120) / 100)
|
|
93
|
+
expect(applySlotMultiplier(computeGathererYield(GATH_STR, 1), 120)).toBe(
|
|
94
|
+
Math.floor((computeGathererYield(GATH_STR, 1) * 120) / 100)
|
|
94
95
|
)
|
|
95
96
|
})
|
|
96
97
|
|
|
@@ -116,7 +117,7 @@ test('resolveLaneCrafter returns correct stats for slot 0 (laneKey=1)', () => {
|
|
|
116
117
|
const layout = getEntityLayout(ITEM_CRAFTER_T1)?.slots ?? []
|
|
117
118
|
const amp = getSlotAmp(layout, 0)
|
|
118
119
|
expect(result.slotIndex).toBe(0)
|
|
119
|
-
expect(result.speed).toBe(applySlotMultiplier(computeCrafterSpeed(CRAFTER_REA), amp))
|
|
120
|
+
expect(result.speed).toBe(applySlotMultiplier(computeCrafterSpeed(CRAFTER_REA, 1), amp))
|
|
120
121
|
expect(result.drain).toBe(computeCrafterDrain(CRAFTER_FIN))
|
|
121
122
|
expect(result.outputPct).toBe(amp)
|
|
122
123
|
})
|
|
@@ -144,7 +145,7 @@ test('resolveLaneLoader returns correct stats for slot 0 (laneKey=1)', () => {
|
|
|
144
145
|
const amp = getSlotAmp(layout, 0)
|
|
145
146
|
expect(result.valid).toBe(true)
|
|
146
147
|
expect(result.slotIndex).toBe(0)
|
|
147
|
-
expect(result.thrust).toBe(applySlotMultiplier(computeLoaderThrust(LOADER_PLA), amp))
|
|
148
|
+
expect(result.thrust).toBe(applySlotMultiplier(computeLoaderThrust(LOADER_PLA, 1), amp))
|
|
148
149
|
expect(result.mass).toBe(computeLoaderMass(LOADER_INS))
|
|
149
150
|
expect(result.outputPct).toBe(amp)
|
|
150
151
|
})
|