@shipload/sdk 1.0.0-next.51 → 1.0.0-next.52
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 +470 -157
- package/lib/shipload.js +2309 -492
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +2279 -491
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +123 -8
- package/lib/testing.js +450 -19
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +451 -20
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/capabilities/crafting.ts +10 -1
- package/src/capabilities/gathering.ts +1 -1
- package/src/capabilities/hauling.ts +0 -5
- package/src/capabilities/modules.ts +6 -0
- package/src/capabilities/movement.ts +1 -1
- package/src/contracts/server.ts +293 -13
- package/src/data/capabilities.ts +11 -2
- package/src/data/entities.json +235 -20
- package/src/data/item-ids.ts +10 -0
- package/src/data/items.json +61 -0
- package/src/data/kind-registry.json +53 -1
- package/src/data/kind-registry.ts +5 -0
- package/src/data/metadata.ts +74 -5
- package/src/data/recipes-runtime.ts +1 -0
- package/src/data/recipes.json +887 -118
- package/src/derivation/capabilities.test.ts +81 -4
- package/src/derivation/capabilities.ts +162 -57
- package/src/derivation/crafting.ts +2 -0
- package/src/derivation/recipe-usage.test.ts +10 -7
- package/src/derivation/rollups.ts +16 -0
- package/src/derivation/stat-scaling.ts +12 -0
- package/src/entities/makers.ts +10 -1
- package/src/index-module.ts +29 -3
- package/src/managers/actions.ts +77 -46
- package/src/managers/construction-types.ts +2 -2
- package/src/managers/construction.ts +15 -15
- package/src/managers/plot.ts +1 -1
- package/src/nft/buildImmutableData.ts +55 -9
- package/src/nft/description.ts +99 -36
- package/src/planner/planner.test.ts +50 -41
- package/src/resolution/resolve-item.test.ts +1 -1
- package/src/resolution/resolve-item.ts +26 -11
- package/src/scheduling/availability.ts +7 -6
- package/src/scheduling/cancel.test.ts +40 -6
- package/src/scheduling/cancel.ts +21 -29
- package/src/scheduling/jobs.ts +71 -0
- package/src/scheduling/lanes.ts +29 -0
- package/src/scheduling/projection.ts +42 -25
- package/src/scheduling/task-cargo.ts +1 -1
- package/src/testing/projection-parity.ts +2 -2
- package/src/travel/reach.ts +4 -6
- package/src/travel/route-planner.ts +60 -49
- package/src/travel/route-simulator.ts +3 -7
- package/src/travel/travel.ts +14 -5
- package/src/types/capabilities.ts +9 -3
- package/src/types.ts +5 -3
- package/src/managers/flatten-gather-plan.test.ts +0 -80
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {expect, test} from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
computeEntityCapabilities,
|
|
4
|
+
computeEngineCapabilities,
|
|
4
5
|
computeGathererCapabilities,
|
|
6
|
+
computeGeneratorCapabilities,
|
|
5
7
|
computeCrafterCapabilities,
|
|
6
8
|
computeLoaderCapabilities,
|
|
7
9
|
computeBaseCapacity,
|
|
@@ -17,12 +19,25 @@ import {
|
|
|
17
19
|
ITEM_GATHERER_T1,
|
|
18
20
|
ITEM_CRAFTER_T1,
|
|
19
21
|
ITEM_LOADER_T1,
|
|
22
|
+
ITEM_SHIP_T1_PACKED,
|
|
23
|
+
ITEM_ROUSTABOUT_T1_PACKED,
|
|
24
|
+
ITEM_PROSPECTOR_T1_PACKED,
|
|
25
|
+
ITEM_TENDER_T1_PACKED,
|
|
26
|
+
ITEM_TUG_T1_PACKED,
|
|
27
|
+
ITEM_PORTER_T1_PACKED,
|
|
28
|
+
ITEM_WRANGLER_T1_PACKED,
|
|
29
|
+
ITEM_DREDGER_T1_PACKED,
|
|
30
|
+
ITEM_WRIGHT_T1_PACKED,
|
|
31
|
+
ITEM_ENGINE_T1,
|
|
32
|
+
ITEM_GENERATOR_T1,
|
|
20
33
|
} from '../data/item-ids'
|
|
21
34
|
import type {InstalledModule} from '../entities/slot-multiplier'
|
|
22
35
|
import type {EntitySlot} from '../data/recipes-runtime'
|
|
36
|
+
import {computeTravelDrain} from '../nft/description'
|
|
37
|
+
import {computeEffectiveModuleStat} from './stat-scaling'
|
|
23
38
|
|
|
24
|
-
function makeGathererStats(strength: number,
|
|
25
|
-
return encodeStats([strength,
|
|
39
|
+
function makeGathererStats(strength: number, tolerance: number, saturation: number): bigint {
|
|
40
|
+
return encodeStats([strength, tolerance, saturation, 0])
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
function makeCrafterStats(fineness: number, conductivity: number): bigint {
|
|
@@ -46,6 +61,24 @@ test('computeBaseCapacity uses container formula for all container-class entitie
|
|
|
46
61
|
}
|
|
47
62
|
})
|
|
48
63
|
|
|
64
|
+
test('computeBaseCapacity uses ship-hull formula for every ship-class entity', () => {
|
|
65
|
+
const stats = {strength: 300, hardness: 400, density: 100}
|
|
66
|
+
const expected = computeBaseCapacity(ITEM_SHIP_T1_PACKED, stats)
|
|
67
|
+
expect(expected).toBeGreaterThan(0)
|
|
68
|
+
for (const itemId of [
|
|
69
|
+
ITEM_ROUSTABOUT_T1_PACKED,
|
|
70
|
+
ITEM_PROSPECTOR_T1_PACKED,
|
|
71
|
+
ITEM_TENDER_T1_PACKED,
|
|
72
|
+
ITEM_TUG_T1_PACKED,
|
|
73
|
+
ITEM_PORTER_T1_PACKED,
|
|
74
|
+
ITEM_WRANGLER_T1_PACKED,
|
|
75
|
+
ITEM_DREDGER_T1_PACKED,
|
|
76
|
+
ITEM_WRIGHT_T1_PACKED,
|
|
77
|
+
]) {
|
|
78
|
+
expect(computeBaseCapacity(itemId, stats)).toBe(expected)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
49
82
|
test('computeEntityCapabilities emits gathererLanes alongside legacy gatherer sum', () => {
|
|
50
83
|
// Two gatherers with distinct stats in separate slots, amp=100 for both
|
|
51
84
|
const gathStats1 = makeGathererStats(300, 200, 400)
|
|
@@ -72,8 +105,8 @@ test('computeEntityCapabilities emits gathererLanes alongside legacy gatherer su
|
|
|
72
105
|
expect(result.gathererLanes![1].slotIndex).toBe(1)
|
|
73
106
|
|
|
74
107
|
// Yields are amp-scaled and distinct
|
|
75
|
-
const caps1 = computeGathererCapabilities({strength: 300,
|
|
76
|
-
const caps2 = computeGathererCapabilities({strength: 500,
|
|
108
|
+
const caps1 = computeGathererCapabilities({strength: 300, tolerance: 200, saturation: 400}, 1)
|
|
109
|
+
const caps2 = computeGathererCapabilities({strength: 500, tolerance: 100, saturation: 300}, 1)
|
|
77
110
|
const expectedYield1 = applySlotMultiplier(caps1.yield, 100)
|
|
78
111
|
const expectedYield2 = applySlotMultiplier(caps2.yield, 100)
|
|
79
112
|
expect(result.gathererLanes![0].yield).toBe(expectedYield1)
|
|
@@ -149,3 +182,47 @@ test('per-lane amp-scaled stats clamp to UInt16, matching the contract clamp_to_
|
|
|
149
182
|
expect(applySlotMultiplier(60000, 200)).toBe(U16_MAX)
|
|
150
183
|
expect(applySlotMultiplier(1000, 150)).toBe(1500)
|
|
151
184
|
})
|
|
185
|
+
|
|
186
|
+
test('generator capacity and recharge are denominated to milli-energy', () => {
|
|
187
|
+
const caps = computeGeneratorCapabilities({resonance: 213, reflectivity: 213})
|
|
188
|
+
expect(caps.capacity).toBe(1_406_500)
|
|
189
|
+
expect(caps.recharge).toBe(3_278)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('engine and generator capabilities use tapered quality consistently', () => {
|
|
193
|
+
const modules: InstalledModule[] = [
|
|
194
|
+
{slotIndex: 0, itemId: ITEM_ENGINE_T1, stats: encodeStats([500, 500])},
|
|
195
|
+
{slotIndex: 1, itemId: ITEM_GENERATOR_T1, stats: encodeStats([500, 500])},
|
|
196
|
+
]
|
|
197
|
+
const layout: EntitySlot[] = [
|
|
198
|
+
{type: 'engine', outputPct: 100, maxTier: 1},
|
|
199
|
+
{type: 'generator', outputPct: 100, maxTier: 1},
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
const result = computeEntityCapabilities({}, ITEM_ROUSTABOUT_T1_PACKED, modules, layout)
|
|
203
|
+
const engines = computeEngineCapabilities({volatility: 500, thermal: 500})
|
|
204
|
+
|
|
205
|
+
expect(result.engines).toEqual({
|
|
206
|
+
thrust: engines.thrust,
|
|
207
|
+
drain: computeTravelDrain(engines.thrust, computeEffectiveModuleStat(500)),
|
|
208
|
+
})
|
|
209
|
+
expect(result.generator).toEqual(
|
|
210
|
+
computeGeneratorCapabilities({resonance: 500, reflectivity: 500})
|
|
211
|
+
)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('gatherer/crafter/hauler drains are denominated', () => {
|
|
215
|
+
expect(computeGathererCapabilities({strength: 0, hardness: 0, saturation: 213}, 1).drain).toBe(
|
|
216
|
+
1_967_500
|
|
217
|
+
)
|
|
218
|
+
expect(computeCrafterCapabilities({fineness: 0, conductivity: 213}).drain).toBe(23_546)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('gatherer depth accepts canonical tolerance and legacy recipe-labelled hardness', () => {
|
|
222
|
+
expect(computeGathererCapabilities({strength: 0, tolerance: 213, saturation: 0}, 2).depth).toBe(
|
|
223
|
+
4_343
|
|
224
|
+
)
|
|
225
|
+
expect(computeGathererCapabilities({strength: 0, hardness: 213, saturation: 0}, 1).depth).toBe(
|
|
226
|
+
1_565
|
|
227
|
+
)
|
|
228
|
+
})
|
|
@@ -1,15 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {getEntityLayout} from '../data/recipes-runtime'
|
|
2
|
+
import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
|
|
3
|
+
import {computeEffectiveModuleStat} from './stat-scaling'
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_BASE_HULLMASS = 100_000
|
|
6
|
+
|
|
7
|
+
export function getBaseHullmassFor(itemId: number): number {
|
|
8
|
+
return getEntityLayout(itemId)?.baseHullmass ?? DEFAULT_BASE_HULLMASS
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isShipHull(itemId: number): boolean {
|
|
12
|
+
return getPackedEntityType(itemId)?.equals(ENTITY_SHIP) ?? false
|
|
3
13
|
}
|
|
4
14
|
|
|
5
|
-
|
|
15
|
+
function sumHullStats(stats: Record<string, number>): number {
|
|
16
|
+
return Object.values(stats).reduce((sum, value) => sum + value, 0)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function computeBaseHullmass(itemId: number, stats: Record<string, number>): number {
|
|
20
|
+
if (isShipHull(itemId)) {
|
|
21
|
+
return Math.floor((getBaseHullmassFor(itemId) * (10_000 - sumHullStats(stats))) / 10_000)
|
|
22
|
+
}
|
|
23
|
+
const lightness = stats.density ?? stats.hardness ?? 0
|
|
24
|
+
return Math.floor((getBaseHullmassFor(itemId) * (2000 - lightness)) / 2000)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function computeShipHullCapabilities(
|
|
28
|
+
stats: Record<string, number>,
|
|
29
|
+
itemId: number = ITEM_SHIP_T1_PACKED
|
|
30
|
+
): {
|
|
6
31
|
hullmass: number
|
|
7
32
|
capacity: number
|
|
8
33
|
} {
|
|
9
|
-
const statSum = (stats
|
|
10
|
-
const exponent = statSum /
|
|
34
|
+
const statSum = sumHullStats(stats)
|
|
35
|
+
const exponent = statSum / 4995.0
|
|
11
36
|
return {
|
|
12
|
-
hullmass: computeBaseHullmass(stats),
|
|
37
|
+
hullmass: computeBaseHullmass(itemId, stats),
|
|
13
38
|
capacity: Math.floor(5000000 * 6 ** exponent),
|
|
14
39
|
}
|
|
15
40
|
}
|
|
@@ -18,8 +43,8 @@ export function computeEngineCapabilities(stats: Record<string, number>): {
|
|
|
18
43
|
thrust: number
|
|
19
44
|
drain: number
|
|
20
45
|
} {
|
|
21
|
-
const vol = stats.volatility
|
|
22
|
-
const thm = stats.thermal
|
|
46
|
+
const vol = computeEffectiveModuleStat(stats.volatility)
|
|
47
|
+
const thm = computeEffectiveModuleStat(stats.thermal)
|
|
23
48
|
|
|
24
49
|
return {
|
|
25
50
|
thrust: 400 + Math.floor((vol * 3) / 4),
|
|
@@ -31,12 +56,12 @@ export function computeGeneratorCapabilities(stats: Record<string, number>): {
|
|
|
31
56
|
capacity: number
|
|
32
57
|
recharge: number
|
|
33
58
|
} {
|
|
34
|
-
const res = stats.resonance
|
|
35
|
-
const ref = stats.reflectivity
|
|
59
|
+
const res = computeEffectiveModuleStat(stats.resonance)
|
|
60
|
+
const ref = computeEffectiveModuleStat(stats.reflectivity)
|
|
36
61
|
|
|
37
62
|
return {
|
|
38
|
-
capacity:
|
|
39
|
-
recharge:
|
|
63
|
+
capacity: 1_300_000 + res * 500,
|
|
64
|
+
recharge: 2000 + ref * 6,
|
|
40
65
|
}
|
|
41
66
|
}
|
|
42
67
|
|
|
@@ -78,12 +103,15 @@ export function computeGathererCapabilities(
|
|
|
78
103
|
} {
|
|
79
104
|
const str = stats.strength
|
|
80
105
|
const con = stats.saturation
|
|
81
|
-
|
|
106
|
+
// Gatherer capabilities are positional in the contract: packed slot 1 is tolerance.
|
|
107
|
+
// T1 recipe presentation historically labels that blended slot as hardness, so retain
|
|
108
|
+
// that fallback for callers that only have recipe-decoded named stats.
|
|
109
|
+
const tol = stats.tolerance ?? stats.hardness
|
|
82
110
|
|
|
83
111
|
return {
|
|
84
112
|
yield: 200 + str,
|
|
85
|
-
drain: 2 * Math.max(
|
|
86
|
-
depth: gathererDepthForTier(
|
|
113
|
+
drain: 2 * Math.max(250_000, 1_250_000 - con * 1250),
|
|
114
|
+
depth: gathererDepthForTier(tol, tier),
|
|
87
115
|
}
|
|
88
116
|
}
|
|
89
117
|
|
|
@@ -111,7 +139,20 @@ export function computeCrafterCapabilities(stats: Record<string, number>): {
|
|
|
111
139
|
|
|
112
140
|
return {
|
|
113
141
|
speed: 100 + Math.floor((fin * 4) / 5),
|
|
114
|
-
drain: Math.max(
|
|
142
|
+
drain: Math.max(5000, 30000 - Math.floor((con * 1000) / 33)),
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function computeBuilderCapabilities(stats: Record<string, number>): {
|
|
147
|
+
speed: number
|
|
148
|
+
drain: number
|
|
149
|
+
} {
|
|
150
|
+
const coh = stats.cohesion
|
|
151
|
+
const tol = stats.tolerance
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
speed: 100 + Math.floor((coh * 4) / 5),
|
|
155
|
+
drain: Math.max(5000, 30000 - Math.floor((tol * 1000) / 33)),
|
|
115
156
|
}
|
|
116
157
|
}
|
|
117
158
|
|
|
@@ -130,7 +171,7 @@ export function computeHaulerCapabilities(
|
|
|
130
171
|
return {
|
|
131
172
|
capacity: computeHaulerCapacity(resonance, tier),
|
|
132
173
|
efficiency: 2000 + plasticity * 6,
|
|
133
|
-
drain:
|
|
174
|
+
drain: computeHaulerDrain(conductivity, tier),
|
|
134
175
|
}
|
|
135
176
|
}
|
|
136
177
|
|
|
@@ -145,16 +186,21 @@ export function computeLauncherCapabilities(
|
|
|
145
186
|
}
|
|
146
187
|
}
|
|
147
188
|
|
|
148
|
-
export function computeStorageCapabilities(
|
|
189
|
+
export function computeStorageCapabilities(
|
|
190
|
+
stats: Record<string, number>,
|
|
191
|
+
tier: number
|
|
192
|
+
): {
|
|
149
193
|
capacity: number
|
|
194
|
+
drain: number
|
|
150
195
|
} {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
196
|
+
return {
|
|
197
|
+
capacity: computeCargoBayCapacity(
|
|
198
|
+
stats.strength ?? 0,
|
|
199
|
+
stats.density ?? 0,
|
|
200
|
+
stats.hardness ?? 0
|
|
201
|
+
),
|
|
202
|
+
drain: computeCargoBayDrain(stats.cohesion ?? 0, tier),
|
|
203
|
+
}
|
|
158
204
|
}
|
|
159
205
|
|
|
160
206
|
export function computeBatteryCapabilities(stats: Record<string, number>): {
|
|
@@ -166,7 +212,7 @@ export function computeBatteryCapabilities(stats: Record<string, number>): {
|
|
|
166
212
|
const insulation = stats.insulation ?? 0
|
|
167
213
|
|
|
168
214
|
const statSum = volatility + thermal + plasticity + insulation
|
|
169
|
-
return {capacity:
|
|
215
|
+
return {capacity: 2_500_000 + Math.floor((statSum * 7_500_000) / 3996)}
|
|
170
216
|
}
|
|
171
217
|
|
|
172
218
|
import {
|
|
@@ -174,10 +220,8 @@ import {
|
|
|
174
220
|
ITEM_CONTAINER_T2_PACKED,
|
|
175
221
|
ITEM_EXTRACTOR_T1_PACKED,
|
|
176
222
|
ITEM_FACTORY_T1_PACKED,
|
|
177
|
-
ITEM_HAULER_SHIP_T2_PACKED,
|
|
178
223
|
ITEM_MASS_CATCHER_T1_PACKED,
|
|
179
224
|
ITEM_MASS_DRIVER_T1_PACKED,
|
|
180
|
-
ITEM_PROSPECTOR_T2_PACKED,
|
|
181
225
|
ITEM_SHIP_T1_PACKED,
|
|
182
226
|
ITEM_WAREHOUSE_T1_PACKED,
|
|
183
227
|
} from '../data/item-ids'
|
|
@@ -190,6 +234,7 @@ import {
|
|
|
190
234
|
MODULE_LOADER,
|
|
191
235
|
MODULE_STORAGE,
|
|
192
236
|
MODULE_CRAFTER,
|
|
237
|
+
MODULE_BUILDER,
|
|
193
238
|
MODULE_HAULER,
|
|
194
239
|
MODULE_WARP,
|
|
195
240
|
MODULE_LAUNCHER,
|
|
@@ -205,7 +250,13 @@ import {
|
|
|
205
250
|
type InstalledModule,
|
|
206
251
|
} from '../entities/slot-multiplier'
|
|
207
252
|
import type {EntitySlot} from '../data/recipes-runtime'
|
|
208
|
-
import {
|
|
253
|
+
import {
|
|
254
|
+
computeCargoBayCapacity,
|
|
255
|
+
computeCargoBayDrain,
|
|
256
|
+
computeHaulerCapacity,
|
|
257
|
+
computeHaulerDrain,
|
|
258
|
+
computeTravelDrain,
|
|
259
|
+
} from '../nft/description'
|
|
209
260
|
|
|
210
261
|
export const CAPACITY_TIER_TABLE = [1.0, 1.4, 1.8, 2.2, 2.6, 3.0, 3.4, 3.8, 4.2, 4.6]
|
|
211
262
|
|
|
@@ -220,25 +271,24 @@ export function applyCapacityTier(baseCapacity: number, tier: number): number {
|
|
|
220
271
|
|
|
221
272
|
export function computeBaseCapacity(itemId: number, stats: Record<string, number>): number {
|
|
222
273
|
let base: number
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return 0
|
|
274
|
+
if (isShipHull(itemId)) {
|
|
275
|
+
base = computeShipHullCapabilities(stats, itemId).capacity
|
|
276
|
+
} else {
|
|
277
|
+
switch (itemId) {
|
|
278
|
+
case ITEM_EXTRACTOR_T1_PACKED:
|
|
279
|
+
case ITEM_FACTORY_T1_PACKED:
|
|
280
|
+
case ITEM_MASS_DRIVER_T1_PACKED:
|
|
281
|
+
case ITEM_MASS_CATCHER_T1_PACKED:
|
|
282
|
+
case ITEM_CONTAINER_T1_PACKED:
|
|
283
|
+
case ITEM_CONTAINER_T2_PACKED:
|
|
284
|
+
base = computeContainerCapabilities(stats).capacity
|
|
285
|
+
break
|
|
286
|
+
case ITEM_WAREHOUSE_T1_PACKED:
|
|
287
|
+
base = computeWarehouseHullCapabilities(stats).capacity
|
|
288
|
+
break
|
|
289
|
+
default:
|
|
290
|
+
return 0
|
|
291
|
+
}
|
|
242
292
|
}
|
|
243
293
|
return applyCapacityTier(base, getItem(itemId).tier)
|
|
244
294
|
}
|
|
@@ -257,7 +307,7 @@ export function computeWarehouseHullCapabilities(stats: Record<string, number>):
|
|
|
257
307
|
const statSum = (stats.strength ?? 0) + (stats.hardness ?? 0)
|
|
258
308
|
const exponent = statSum / 1998.0
|
|
259
309
|
return {
|
|
260
|
-
hullmass: computeBaseHullmass(stats),
|
|
310
|
+
hullmass: computeBaseHullmass(ITEM_WAREHOUSE_T1_PACKED, stats),
|
|
261
311
|
capacity: Math.floor(100000000 * 6 ** exponent),
|
|
262
312
|
}
|
|
263
313
|
}
|
|
@@ -277,6 +327,13 @@ export interface CrafterLaneEntry {
|
|
|
277
327
|
outputPct: number
|
|
278
328
|
}
|
|
279
329
|
|
|
330
|
+
export interface BuilderLaneEntry {
|
|
331
|
+
slotIndex: number
|
|
332
|
+
speed: number
|
|
333
|
+
drain: number
|
|
334
|
+
outputPct: number
|
|
335
|
+
}
|
|
336
|
+
|
|
280
337
|
export interface LoaderLaneEntry {
|
|
281
338
|
slotIndex: number
|
|
282
339
|
mass: number
|
|
@@ -284,10 +341,18 @@ export interface LoaderLaneEntry {
|
|
|
284
341
|
outputPct: number
|
|
285
342
|
}
|
|
286
343
|
|
|
344
|
+
export interface TravelDrainBreakdown {
|
|
345
|
+
engine: number
|
|
346
|
+
cargoHolds: number
|
|
347
|
+
tractorBeams: number
|
|
348
|
+
total: number
|
|
349
|
+
}
|
|
350
|
+
|
|
287
351
|
export interface ComputedCapabilities {
|
|
288
352
|
hullmass: number
|
|
289
353
|
capacity: number
|
|
290
354
|
engines?: {thrust: number; drain: number}
|
|
355
|
+
travelDrain?: TravelDrainBreakdown
|
|
291
356
|
generator?: {capacity: number; recharge: number}
|
|
292
357
|
gatherer?: {yield: number; drain: number; depth: number}
|
|
293
358
|
gathererLanes?: GathererLaneEntry[]
|
|
@@ -295,6 +360,8 @@ export interface ComputedCapabilities {
|
|
|
295
360
|
loaderLanes?: LoaderLaneEntry[]
|
|
296
361
|
crafter?: {speed: number; drain: number}
|
|
297
362
|
crafterLanes?: CrafterLaneEntry[]
|
|
363
|
+
builder?: {speed: number; drain: number}
|
|
364
|
+
builderLanes?: BuilderLaneEntry[]
|
|
298
365
|
hauler?: {
|
|
299
366
|
capacity: number
|
|
300
367
|
efficiency: number
|
|
@@ -331,6 +398,7 @@ export function computeEntityCapabilities(
|
|
|
331
398
|
let hasGatherer = false
|
|
332
399
|
|
|
333
400
|
let totalStorageCapacity = 0
|
|
401
|
+
let totalCargoHoldDrain = 0
|
|
334
402
|
const baseCapacity = computeBaseCapacity(itemId, stats)
|
|
335
403
|
let installedModuleMass = 0
|
|
336
404
|
|
|
@@ -338,6 +406,10 @@ export function computeEntityCapabilities(
|
|
|
338
406
|
let totalCrafterDrain = 0
|
|
339
407
|
let hasCrafter = false
|
|
340
408
|
|
|
409
|
+
let totalBuilderSpeed = 0
|
|
410
|
+
let totalBuilderDrain = 0
|
|
411
|
+
let hasBuilder = false
|
|
412
|
+
|
|
341
413
|
let totalHaulerCapacity = 0
|
|
342
414
|
let weightedHaulerEffNum = 0n
|
|
343
415
|
let totalHaulerDrain = 0
|
|
@@ -356,6 +428,7 @@ export function computeEntityCapabilities(
|
|
|
356
428
|
|
|
357
429
|
const gathererLanes: GathererLaneEntry[] = []
|
|
358
430
|
const crafterLanes: CrafterLaneEntry[] = []
|
|
431
|
+
const builderLanes: BuilderLaneEntry[] = []
|
|
359
432
|
const loaderLanes: LoaderLaneEntry[] = []
|
|
360
433
|
|
|
361
434
|
for (const mod of modules) {
|
|
@@ -369,17 +442,24 @@ export function computeEntityCapabilities(
|
|
|
369
442
|
hasEngine = true
|
|
370
443
|
const caps = computeEngineCapabilities(decodedStats)
|
|
371
444
|
totalThrust += applySlotMultiplier(caps.thrust, amp)
|
|
372
|
-
totalEngineThm += decodedStats.thermal ?? 0
|
|
445
|
+
totalEngineThm += computeEffectiveModuleStat(decodedStats.thermal ?? 0)
|
|
373
446
|
engineCount += 1
|
|
374
447
|
} else if (modType === MODULE_GENERATOR) {
|
|
375
448
|
hasGenerator = true
|
|
376
449
|
const caps = computeGeneratorCapabilities(decodedStats)
|
|
377
|
-
totalGenCapacity +=
|
|
378
|
-
totalGenRecharge +=
|
|
450
|
+
totalGenCapacity += applySlotMultiplierUint32(caps.capacity, amp)
|
|
451
|
+
totalGenRecharge += applySlotMultiplierUint32(caps.recharge, amp)
|
|
379
452
|
} else if (modType === MODULE_GATHERER) {
|
|
380
453
|
hasGatherer = true
|
|
381
454
|
const tier = item.tier
|
|
382
|
-
const caps = computeGathererCapabilities(
|
|
455
|
+
const caps = computeGathererCapabilities(
|
|
456
|
+
{
|
|
457
|
+
strength: decodeStat(mod.stats, 0),
|
|
458
|
+
tolerance: decodeStat(mod.stats, 1),
|
|
459
|
+
saturation: decodeStat(mod.stats, 2),
|
|
460
|
+
},
|
|
461
|
+
tier
|
|
462
|
+
)
|
|
383
463
|
const scaledYield = applySlotMultiplier(caps.yield, amp)
|
|
384
464
|
totalGathYield += scaledYield
|
|
385
465
|
totalGathDrain += caps.drain
|
|
@@ -404,8 +484,9 @@ export function computeEntityCapabilities(
|
|
|
404
484
|
outputPct: amp,
|
|
405
485
|
})
|
|
406
486
|
} else if (modType === MODULE_STORAGE) {
|
|
407
|
-
const caps = computeStorageCapabilities(decodedStats)
|
|
487
|
+
const caps = computeStorageCapabilities(decodedStats, item.tier)
|
|
408
488
|
totalStorageCapacity += applySlotMultiplierUint32(caps.capacity, amp)
|
|
489
|
+
totalCargoHoldDrain += caps.drain
|
|
409
490
|
} else if (modType === MODULE_CRAFTER) {
|
|
410
491
|
hasCrafter = true
|
|
411
492
|
const caps = computeCrafterCapabilities(decodedStats)
|
|
@@ -418,6 +499,18 @@ export function computeEntityCapabilities(
|
|
|
418
499
|
drain: caps.drain,
|
|
419
500
|
outputPct: amp,
|
|
420
501
|
})
|
|
502
|
+
} else if (modType === MODULE_BUILDER) {
|
|
503
|
+
hasBuilder = true
|
|
504
|
+
const caps = computeBuilderCapabilities(decodedStats)
|
|
505
|
+
const scaledSpeed = applySlotMultiplier(caps.speed, amp)
|
|
506
|
+
totalBuilderSpeed += scaledSpeed
|
|
507
|
+
totalBuilderDrain += caps.drain
|
|
508
|
+
builderLanes.push({
|
|
509
|
+
slotIndex: mod.slotIndex,
|
|
510
|
+
speed: scaledSpeed,
|
|
511
|
+
drain: caps.drain,
|
|
512
|
+
outputPct: amp,
|
|
513
|
+
})
|
|
421
514
|
} else if (modType === MODULE_HAULER) {
|
|
422
515
|
hasHauler = true
|
|
423
516
|
const caps = computeHaulerCapabilities(decodedStats, item.tier)
|
|
@@ -454,13 +547,21 @@ export function computeEntityCapabilities(
|
|
|
454
547
|
}
|
|
455
548
|
|
|
456
549
|
const result: ComputedCapabilities = {
|
|
457
|
-
hullmass: computeBaseHullmass(stats) + installedModuleMass,
|
|
550
|
+
hullmass: computeBaseHullmass(itemId, stats) + installedModuleMass,
|
|
458
551
|
capacity: clampUint32(baseCapacity + totalStorageCapacity),
|
|
459
552
|
}
|
|
460
553
|
|
|
461
554
|
if (hasEngine) {
|
|
462
555
|
const avgThm = engineCount > 0 ? Math.trunc(totalEngineThm / engineCount) : 0
|
|
463
|
-
|
|
556
|
+
const engineDrain = computeTravelDrain(totalThrust, avgThm)
|
|
557
|
+
const totalDrain = clampUint32(engineDrain + totalCargoHoldDrain + totalHaulerDrain)
|
|
558
|
+
result.engines = {thrust: totalThrust, drain: totalDrain}
|
|
559
|
+
result.travelDrain = {
|
|
560
|
+
engine: engineDrain,
|
|
561
|
+
cargoHolds: totalCargoHoldDrain,
|
|
562
|
+
tractorBeams: totalHaulerDrain,
|
|
563
|
+
total: totalDrain,
|
|
564
|
+
}
|
|
464
565
|
}
|
|
465
566
|
if (hasGenerator) {
|
|
466
567
|
result.generator = {
|
|
@@ -488,6 +589,10 @@ export function computeEntityCapabilities(
|
|
|
488
589
|
result.crafter = {speed: clampUint16(totalCrafterSpeed), drain: totalCrafterDrain}
|
|
489
590
|
result.crafterLanes = crafterLanes
|
|
490
591
|
}
|
|
592
|
+
if (hasBuilder) {
|
|
593
|
+
result.builder = {speed: clampUint16(totalBuilderSpeed), drain: totalBuilderDrain}
|
|
594
|
+
result.builderLanes = builderLanes
|
|
595
|
+
}
|
|
491
596
|
if (hasHauler) {
|
|
492
597
|
const efficiency =
|
|
493
598
|
totalHaulerCapacity > 0 ? Number(weightedHaulerEffNum / BigInt(totalHaulerCapacity)) : 0
|
|
@@ -521,7 +626,7 @@ export function computeContainerCapabilities(stats: Record<string, number>): {
|
|
|
521
626
|
const statSum = (stats.strength ?? 0) + (stats.hardness ?? 0)
|
|
522
627
|
const exponent = statSum / 1998.0
|
|
523
628
|
return {
|
|
524
|
-
hullmass: computeBaseHullmass(stats),
|
|
629
|
+
hullmass: computeBaseHullmass(ITEM_CONTAINER_T1_PACKED, stats),
|
|
525
630
|
capacity: Math.floor(22000000 * 6 ** exponent),
|
|
526
631
|
}
|
|
527
632
|
}
|
|
@@ -4,6 +4,7 @@ import {getRecipe, type Recipe} from '../data/recipes-runtime'
|
|
|
4
4
|
import {getItem} from '../data/catalog'
|
|
5
5
|
import {getStatDefinitions} from './stats'
|
|
6
6
|
import {deriveResourceStats} from './stratum'
|
|
7
|
+
import {ITEM_SHIP_T1_PACKED} from '../data/item-ids'
|
|
7
8
|
|
|
8
9
|
export interface StackInput {
|
|
9
10
|
quantity: number
|
|
@@ -41,6 +42,7 @@ function getItemStatKeys(itemId: number): string[] {
|
|
|
41
42
|
if (!item.category) return []
|
|
42
43
|
return getStatDefinitions(item.category).map((d) => d.key)
|
|
43
44
|
}
|
|
45
|
+
if (itemId === ITEM_SHIP_T1_PACKED) return ['strength', 'density', '', '']
|
|
44
46
|
const recipe = getRecipe(itemId)
|
|
45
47
|
if (!recipe) return []
|
|
46
48
|
return recipe.statSlots.map((slot) => keyForStatSlot(recipe, slot))
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
} from './recipe-usage'
|
|
8
8
|
import {
|
|
9
9
|
ITEM_SENSOR,
|
|
10
|
-
ITEM_SENSOR_T2,
|
|
11
10
|
ITEM_RESIN,
|
|
12
11
|
ITEM_FRAME,
|
|
13
12
|
ITEM_PLATE,
|
|
@@ -15,7 +14,13 @@ import {
|
|
|
15
14
|
ITEM_GATHERER_T1,
|
|
16
15
|
ITEM_CRAFTER_T1,
|
|
17
16
|
ITEM_EXTRACTOR_T1_PACKED,
|
|
18
|
-
|
|
17
|
+
ITEM_ROUSTABOUT_T1_PACKED,
|
|
18
|
+
ITEM_PROSPECTOR_T1_PACKED,
|
|
19
|
+
ITEM_TENDER_T1_PACKED,
|
|
20
|
+
ITEM_TUG_T1_PACKED,
|
|
21
|
+
ITEM_PORTER_T1_PACKED,
|
|
22
|
+
ITEM_WRANGLER_T1_PACKED,
|
|
23
|
+
ITEM_DREDGER_T1_PACKED,
|
|
19
24
|
} from '../data/item-ids'
|
|
20
25
|
|
|
21
26
|
test('getAllRecipes returns the full catalog including the gatherer', () => {
|
|
@@ -28,9 +33,7 @@ test('getRecipeConsumers lists every recipe that consumes Sensor', () => {
|
|
|
28
33
|
const consumers = getRecipeConsumers(ITEM_SENSOR)
|
|
29
34
|
const ids = consumers.map((c) => c.outputItemId).sort((a, b) => a - b)
|
|
30
35
|
expect(ids).toEqual(
|
|
31
|
-
[ITEM_CRAFTER_T1,
|
|
32
|
-
(a, b) => a - b
|
|
33
|
-
)
|
|
36
|
+
[ITEM_CRAFTER_T1, ITEM_EXTRACTOR_T1_PACKED, ITEM_ROUSTABOUT_T1_PACKED].sort((a, b) => a - b)
|
|
34
37
|
)
|
|
35
38
|
})
|
|
36
39
|
|
|
@@ -73,9 +76,9 @@ test('getResourceDemand scales by quantity', () => {
|
|
|
73
76
|
expect(getResourceDemand(ITEM_PLATE, 3)).toEqual({ore: 30})
|
|
74
77
|
})
|
|
75
78
|
|
|
76
|
-
test('getComponentDemand reports Resin as consumed by
|
|
79
|
+
test('getComponentDemand reports Resin as consumed by nine recipes', () => {
|
|
77
80
|
const demand = getComponentDemand()
|
|
78
81
|
const resin = demand.find((d) => d.itemId === ITEM_RESIN)
|
|
79
82
|
expect(resin).toBeDefined()
|
|
80
|
-
expect(resin?.consumerCount).toBe(
|
|
83
|
+
expect(resin?.consumerCount).toBe(9)
|
|
81
84
|
})
|
|
@@ -37,6 +37,22 @@ export function rollupCrafter(
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export function rollupBuilder(
|
|
41
|
+
lanes: ServerContract.Types.builder_lane[]
|
|
42
|
+
): {speed: UInt16; drain: UInt32} | undefined {
|
|
43
|
+
if (lanes.length === 0) return undefined
|
|
44
|
+
let totalSpeed = 0
|
|
45
|
+
let totalDrain = 0
|
|
46
|
+
for (const l of lanes) {
|
|
47
|
+
totalSpeed += Number(l.speed)
|
|
48
|
+
totalDrain += Number(l.drain)
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
speed: UInt16.from(Math.min(totalSpeed, 65535)),
|
|
52
|
+
drain: UInt32.from(totalDrain),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
export function rollupLoaders(
|
|
41
57
|
lanes: ServerContract.Types.loader_lane[]
|
|
42
58
|
): {mass: UInt32; thrust: UInt16; quantity: UInt8} | undefined {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const MODULE_STAT_SCALING_ANCHOR = 213
|
|
2
|
+
export const MODULE_STAT_SCALING_POST_ANCHOR_PERCENT = 40
|
|
3
|
+
|
|
4
|
+
export function computeEffectiveModuleStat(stat: number): number {
|
|
5
|
+
if (stat <= MODULE_STAT_SCALING_ANCHOR) return stat
|
|
6
|
+
return (
|
|
7
|
+
MODULE_STAT_SCALING_ANCHOR +
|
|
8
|
+
Math.floor(
|
|
9
|
+
((stat - MODULE_STAT_SCALING_ANCHOR) * MODULE_STAT_SCALING_POST_ANCHOR_PERCENT) / 100
|
|
10
|
+
)
|
|
11
|
+
)
|
|
12
|
+
}
|
package/src/entities/makers.ts
CHANGED
|
@@ -112,7 +112,7 @@ export function makeEntity(packedItemId: number, state: EntityStateInput): Entit
|
|
|
112
112
|
holds: [],
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
if (state.energy !== undefined) info.energy =
|
|
115
|
+
if (state.energy !== undefined) info.energy = UInt32.from(state.energy)
|
|
116
116
|
|
|
117
117
|
if (kind === 'container') {
|
|
118
118
|
info.modules = []
|
|
@@ -174,6 +174,14 @@ export function makeEntity(packedItemId: number, state: EntityStateInput): Entit
|
|
|
174
174
|
output_pct: l.outputPct,
|
|
175
175
|
})
|
|
176
176
|
)
|
|
177
|
+
info.builder_lanes = (caps.builderLanes ?? []).map((l) =>
|
|
178
|
+
ServerContract.Types.builder_lane.from({
|
|
179
|
+
slot_index: l.slotIndex,
|
|
180
|
+
speed: l.speed,
|
|
181
|
+
drain: l.drain,
|
|
182
|
+
output_pct: l.outputPct,
|
|
183
|
+
})
|
|
184
|
+
)
|
|
177
185
|
info.loader_lanes = (caps.loaderLanes ?? []).map((l) =>
|
|
178
186
|
ServerContract.Types.loader_lane.from({
|
|
179
187
|
slot_index: l.slotIndex,
|
|
@@ -186,6 +194,7 @@ export function makeEntity(packedItemId: number, state: EntityStateInput): Entit
|
|
|
186
194
|
|
|
187
195
|
if (!info.gatherer_lanes) info.gatherer_lanes = []
|
|
188
196
|
if (!info.crafter_lanes) info.crafter_lanes = []
|
|
197
|
+
if (!info.builder_lanes) info.builder_lanes = []
|
|
189
198
|
if (!info.loader_lanes) info.loader_lanes = []
|
|
190
199
|
|
|
191
200
|
const entityInfo = ServerContract.Types.entity_info.from(info)
|