@shipload/sdk 1.0.0-next.6 → 1.0.0-next.61
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 +52 -0
- package/lib/scan.js +162 -0
- package/lib/scan.js.map +1 -0
- package/lib/scan.m.js +152 -0
- package/lib/scan.m.js.map +1 -0
- package/lib/shipload.d.ts +3601 -1364
- package/lib/shipload.js +15671 -5261
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +15392 -5209
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +1173 -0
- package/lib/testing.js +4971 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +4965 -0
- package/lib/testing.m.js.map +1 -0
- package/package.json +20 -2
- package/src/capabilities/craftable.test.ts +82 -0
- package/src/capabilities/craftable.ts +82 -0
- package/src/capabilities/crafting.test.ts +35 -0
- package/src/capabilities/crafting.ts +34 -7
- package/src/capabilities/gathering.test.ts +29 -0
- package/src/capabilities/gathering.ts +37 -19
- package/src/capabilities/hauling.ts +0 -5
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +27 -30
- package/src/capabilities/movement.ts +1 -1
- package/src/capabilities/storage.ts +16 -1
- package/src/contracts/platform.ts +231 -3
- package/src/contracts/server.ts +1518 -501
- 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 +49 -18
- package/src/data/capability-formulas.ts +26 -14
- package/src/data/catalog.ts +7 -8
- package/src/data/colors.ts +14 -48
- package/src/data/entities.json +380 -15
- package/src/data/item-ids.ts +51 -13
- package/src/data/items.json +318 -77
- package/src/data/kind-registry.json +195 -0
- package/src/data/kind-registry.ts +182 -0
- package/src/data/metadata.ts +291 -51
- package/src/data/recipes-runtime.ts +6 -23
- package/src/data/recipes.json +2260 -173
- package/src/data/tiers.ts +9 -6
- package/src/derivation/build-methods.test.ts +13 -0
- package/src/derivation/build-methods.ts +48 -0
- package/src/derivation/capabilities.test.ts +348 -0
- package/src/derivation/capabilities.ts +693 -0
- package/src/derivation/capability-mappings.ts +60 -16
- package/src/derivation/crafting.test.ts +17 -0
- package/src/derivation/crafting.ts +57 -34
- package/src/derivation/index.ts +27 -2
- package/src/derivation/recipe-usage.test.ts +90 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +9 -1
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +72 -0
- package/src/derivation/stars.test.ts +67 -0
- package/src/derivation/stars.ts +25 -0
- package/src/derivation/stat-scaling.ts +12 -0
- package/src/derivation/stats.ts +6 -6
- package/src/derivation/stratum.ts +26 -22
- package/src/derivation/tiers.ts +40 -7
- package/src/derivation/upgrades.ts +14 -0
- package/src/derivation/wormhole.ts +154 -0
- package/src/entities/entity.ts +102 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +141 -135
- 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 +333 -35
- package/src/managers/actions.ts +668 -103
- package/src/managers/base.ts +6 -2
- package/src/managers/cluster.test.ts +39 -0
- package/src/managers/cluster.ts +53 -0
- package/src/managers/construction-types.ts +80 -0
- package/src/managers/construction.ts +443 -0
- package/src/managers/context.ts +29 -1
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/entities.ts +27 -66
- package/src/managers/epochs.ts +40 -0
- package/src/managers/index.ts +20 -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/players.ts +25 -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 +388 -0
- package/src/nft/description.ts +215 -99
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +242 -0
- package/src/planner/planner.test.ts +334 -0
- package/src/resolution/describe-module.ts +43 -25
- package/src/resolution/display-name.ts +38 -10
- package/src/resolution/resolve-item.test.ts +48 -0
- package/src/resolution/resolve-item.ts +112 -58
- package/src/scan/index.ts +244 -0
- package/src/scan/scan-wasm.base64.ts +2 -0
- package/src/scheduling/accessor.ts +65 -23
- package/src/scheduling/availability.test.ts +204 -0
- package/src/scheduling/availability.ts +211 -0
- package/src/scheduling/cancel.test.ts +542 -0
- package/src/scheduling/cancel.ts +254 -0
- package/src/scheduling/cluster-stock.test.ts +31 -0
- package/src/scheduling/cluster-stock.ts +15 -0
- package/src/scheduling/energy.ts +47 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/jobs.ts +71 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +250 -0
- package/src/scheduling/lanes.ts +227 -0
- package/src/scheduling/projection.ts +257 -133
- package/src/scheduling/schedule.test.ts +119 -0
- package/src/scheduling/schedule.ts +257 -117
- package/src/scheduling/task-cargo.test.ts +44 -0
- package/src/scheduling/task-cargo.ts +46 -0
- package/src/scheduling/unwrap.test.ts +86 -0
- package/src/scheduling/unwrap.ts +190 -0
- package/src/shipload.ts +26 -1
- package/src/subscriptions/manager.cluster.test.ts +51 -0
- package/src/subscriptions/manager.ts +245 -172
- package/src/subscriptions/mappers.ts +5 -8
- package/src/subscriptions/types.ts +28 -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 +21 -0
- package/src/travel/route-planner.ts +288 -0
- package/src/travel/route-simulator.ts +170 -0
- package/src/travel/travel.ts +188 -122
- package/src/types/capabilities.ts +37 -8
- package/src/types/entity.ts +3 -3
- package/src/types/index.ts +0 -1
- package/src/types.ts +51 -16
- package/src/utils/cargo.test.ts +14 -0
- package/src/utils/cargo.ts +29 -0
- package/src/utils/display-name.ts +70 -0
- package/src/utils/system.ts +15 -16
- package/src/capabilities/loading.ts +0 -8
- package/src/entities/container.ts +0 -123
- package/src/entities/ship-deploy.ts +0 -295
- package/src/entities/ship.ts +0 -221
- package/src/entities/warehouse.ts +0 -127
- package/src/types/entity-traits.ts +0 -69
package/src/data/tiers.ts
CHANGED
|
@@ -18,17 +18,20 @@ export function itemTypeCode(id: number): number {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const CRAFTED_BASE = 10000
|
|
22
|
+
const TIER_STRIDE = 1000
|
|
23
|
+
|
|
21
24
|
export function itemTier(id: number): number {
|
|
22
|
-
if (id <
|
|
23
|
-
return Math.floor(id /
|
|
25
|
+
if (id < CRAFTED_BASE) return 0
|
|
26
|
+
return Math.floor((id - CRAFTED_BASE) / TIER_STRIDE) + 1
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export function itemOffset(id: number): number {
|
|
27
|
-
return id %
|
|
30
|
+
return id % TIER_STRIDE
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export function itemCategory(id: number): CraftedItemCategory {
|
|
31
|
-
if (id <
|
|
34
|
+
if (id < CRAFTED_BASE) return 'resource'
|
|
32
35
|
const offset = itemOffset(id)
|
|
33
36
|
if (offset >= 200) return 'entity'
|
|
34
37
|
if (offset >= 100) return 'module'
|
|
@@ -36,10 +39,10 @@ export function itemCategory(id: number): CraftedItemCategory {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export function isRelatedItem(a: number, b: number): boolean {
|
|
39
|
-
if (a <
|
|
42
|
+
if (a < CRAFTED_BASE || b < CRAFTED_BASE) return false
|
|
40
43
|
return itemOffset(a) === itemOffset(b)
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export function isCraftedItem(id: number): boolean {
|
|
44
|
-
return id >=
|
|
47
|
+
return id >= CRAFTED_BASE
|
|
45
48
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {availableBuildMethods} from './build-methods'
|
|
3
|
+
import {ITEM_HUB_T1_PACKED, ITEM_WAREHOUSE_T1_PACKED} from '../data/item-ids'
|
|
4
|
+
|
|
5
|
+
describe('availableBuildMethods', () => {
|
|
6
|
+
test('orbital structures build via craft+deploy or plot', () => {
|
|
7
|
+
expect(availableBuildMethods(ITEM_WAREHOUSE_T1_PACKED)).toEqual(['craft+deploy', 'plot'])
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('hub is craft+deploy only — excluded from the plot path', () => {
|
|
11
|
+
expect(availableBuildMethods(ITEM_HUB_T1_PACKED)).toEqual(['craft+deploy'])
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {ENTITY_HUB, EntityClass, getKindMeta, getTemplateMeta} from '../data/kind-registry'
|
|
2
|
+
import {getRecipe} from '../data/recipes-runtime'
|
|
3
|
+
import {getItems} from '../data/catalog'
|
|
4
|
+
import type {Item} from '../types'
|
|
5
|
+
|
|
6
|
+
export type BuildMethod = 'craft+deploy' | 'plot'
|
|
7
|
+
|
|
8
|
+
export function availableBuildMethods(itemId: number): BuildMethod[] {
|
|
9
|
+
const recipe = getRecipe(itemId)
|
|
10
|
+
if (!recipe) return []
|
|
11
|
+
|
|
12
|
+
const template = getTemplateMeta(itemId)
|
|
13
|
+
if (!template) return ['craft+deploy']
|
|
14
|
+
|
|
15
|
+
const kindMeta = getKindMeta(template.kind)
|
|
16
|
+
if (!kindMeta) return ['craft+deploy']
|
|
17
|
+
|
|
18
|
+
if (
|
|
19
|
+
kindMeta.classification === EntityClass.OrbitalStructure &&
|
|
20
|
+
!kindMeta.kind.equals(ENTITY_HUB)
|
|
21
|
+
) {
|
|
22
|
+
return ['craft+deploy', 'plot']
|
|
23
|
+
}
|
|
24
|
+
return ['craft+deploy']
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isBuildable(itemId: number): boolean {
|
|
28
|
+
return availableBuildMethods(itemId).length > 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isPlotBuildable(itemId: number): boolean {
|
|
32
|
+
return availableBuildMethods(itemId).includes('plot')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function filterByBuildMethod<T extends {itemId: number}>(
|
|
36
|
+
items: T[],
|
|
37
|
+
method: BuildMethod
|
|
38
|
+
): T[] {
|
|
39
|
+
return items.filter((i) => availableBuildMethods(i.itemId).includes(method))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function allBuildableItems(): Item[] {
|
|
43
|
+
return getItems().filter((item) => isBuildable(item.id))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function allPlotBuildableItems(): Item[] {
|
|
47
|
+
return getItems().filter((item) => isPlotBuildable(item.id))
|
|
48
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
computeEntityCapabilities,
|
|
4
|
+
computeEngineCapabilities,
|
|
5
|
+
computeGathererCapabilities,
|
|
6
|
+
computeGathererYield,
|
|
7
|
+
computeGeneratorCapabilities,
|
|
8
|
+
computeCrafterCapabilities,
|
|
9
|
+
computeBuilderCapabilities,
|
|
10
|
+
computeLoaderCapabilities,
|
|
11
|
+
computeBaseCapacity,
|
|
12
|
+
computeContainerCapabilities,
|
|
13
|
+
GATHERER_YIELD_TIER_TABLE,
|
|
14
|
+
ENGINE_THRUST_TIER_PCT,
|
|
15
|
+
GENERATOR_CAPACITY_TIER_PCT,
|
|
16
|
+
GENERATOR_RECHARGE_TIER_PCT,
|
|
17
|
+
CRAFTER_SPEED_TIER_PCT,
|
|
18
|
+
BUILDER_SPEED_TIER_PCT,
|
|
19
|
+
WARP_RANGE_TIER_PCT,
|
|
20
|
+
LOADER_THRUST_TIER_PCT,
|
|
21
|
+
moduleTierPct,
|
|
22
|
+
} from './capabilities'
|
|
23
|
+
import {
|
|
24
|
+
computeEngineThrust,
|
|
25
|
+
computeGeneratorCap,
|
|
26
|
+
computeGeneratorRech,
|
|
27
|
+
computeCrafterSpeed,
|
|
28
|
+
computeBuilderSpeed,
|
|
29
|
+
computeWarpRange,
|
|
30
|
+
computeLoaderThrust,
|
|
31
|
+
} from '../nft/description'
|
|
32
|
+
import {applySlotMultiplier, U16_MAX} from '../entities/slot-multiplier'
|
|
33
|
+
import {encodeStats} from './crafting'
|
|
34
|
+
import {
|
|
35
|
+
ITEM_EXTRACTOR_T1_PACKED,
|
|
36
|
+
ITEM_FACTORY_T1_PACKED,
|
|
37
|
+
ITEM_MASS_DRIVER_T1_PACKED,
|
|
38
|
+
ITEM_MASS_CATCHER_T1_PACKED,
|
|
39
|
+
ITEM_GATHERER_T1,
|
|
40
|
+
ITEM_CRAFTER_T1,
|
|
41
|
+
ITEM_LOADER_T1,
|
|
42
|
+
ITEM_SHIP_T1_PACKED,
|
|
43
|
+
ITEM_ROUSTABOUT_T1A_PACKED,
|
|
44
|
+
ITEM_PROSPECTOR_T1A_PACKED,
|
|
45
|
+
ITEM_TENDER_T1A_PACKED,
|
|
46
|
+
ITEM_TUG_T1A_PACKED,
|
|
47
|
+
ITEM_PORTER_T1A_PACKED,
|
|
48
|
+
ITEM_SMITH_T1A_PACKED,
|
|
49
|
+
ITEM_WRIGHT_T1A_PACKED,
|
|
50
|
+
ITEM_ENGINE_T1,
|
|
51
|
+
ITEM_GENERATOR_T1,
|
|
52
|
+
ITEM_BUILDER_T1,
|
|
53
|
+
} from '../data/item-ids'
|
|
54
|
+
import type {InstalledModule} from '../entities/slot-multiplier'
|
|
55
|
+
import type {EntitySlot} from '../data/recipes-runtime'
|
|
56
|
+
import {computeTravelDrain} from '../nft/description'
|
|
57
|
+
import {computeEffectiveModuleStat} from './stat-scaling'
|
|
58
|
+
|
|
59
|
+
function makeGathererStats(strength: number, tolerance: number, saturation: number): bigint {
|
|
60
|
+
return encodeStats([strength, tolerance, saturation, 0])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function makeCrafterStats(fineness: number, conductivity: number): bigint {
|
|
64
|
+
return encodeStats([fineness, conductivity])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function makeLoaderStats(insulation: number, plasticity: number): bigint {
|
|
68
|
+
return encodeStats([insulation, plasticity])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function makeBuilderStats(resonance: number, fineness: number): bigint {
|
|
72
|
+
return encodeStats([resonance, fineness])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
test('computeBaseCapacity uses container formula for all container-class entities', () => {
|
|
76
|
+
const stats = {strength: 300, hardness: 400, density: 100}
|
|
77
|
+
const expected = computeContainerCapabilities(stats).capacity
|
|
78
|
+
for (const itemId of [
|
|
79
|
+
ITEM_EXTRACTOR_T1_PACKED,
|
|
80
|
+
ITEM_FACTORY_T1_PACKED,
|
|
81
|
+
ITEM_MASS_DRIVER_T1_PACKED,
|
|
82
|
+
ITEM_MASS_CATCHER_T1_PACKED,
|
|
83
|
+
]) {
|
|
84
|
+
expect(computeBaseCapacity(itemId, stats)).toBe(expected)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('computeBaseCapacity uses ship-hull formula for every ship-class entity', () => {
|
|
89
|
+
const stats = {strength: 300, hardness: 400, density: 100}
|
|
90
|
+
const expected = computeBaseCapacity(ITEM_SHIP_T1_PACKED, stats)
|
|
91
|
+
expect(expected).toBeGreaterThan(0)
|
|
92
|
+
for (const itemId of [
|
|
93
|
+
ITEM_ROUSTABOUT_T1A_PACKED,
|
|
94
|
+
ITEM_PROSPECTOR_T1A_PACKED,
|
|
95
|
+
ITEM_TENDER_T1A_PACKED,
|
|
96
|
+
ITEM_TUG_T1A_PACKED,
|
|
97
|
+
ITEM_PORTER_T1A_PACKED,
|
|
98
|
+
ITEM_SMITH_T1A_PACKED,
|
|
99
|
+
ITEM_WRIGHT_T1A_PACKED,
|
|
100
|
+
]) {
|
|
101
|
+
expect(computeBaseCapacity(itemId, stats)).toBe(expected)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('computeEntityCapabilities emits gathererLanes alongside legacy gatherer sum', () => {
|
|
106
|
+
// Two gatherers with distinct stats in separate slots, amp=100 for both
|
|
107
|
+
const gathStats1 = makeGathererStats(300, 200, 400)
|
|
108
|
+
const gathStats2 = makeGathererStats(500, 100, 300)
|
|
109
|
+
|
|
110
|
+
const modules: InstalledModule[] = [
|
|
111
|
+
{slotIndex: 0, itemId: ITEM_GATHERER_T1, stats: gathStats1},
|
|
112
|
+
{slotIndex: 1, itemId: ITEM_GATHERER_T1, stats: gathStats2},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
const layout: EntitySlot[] = [
|
|
116
|
+
{type: 'gatherer', outputPct: 100, maxTier: 1},
|
|
117
|
+
{type: 'gatherer', outputPct: 100, maxTier: 1},
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
const result = computeEntityCapabilities({}, ITEM_EXTRACTOR_T1_PACKED, modules, layout)
|
|
121
|
+
|
|
122
|
+
// Lane lists must exist
|
|
123
|
+
expect(result.gathererLanes).toBeDefined()
|
|
124
|
+
expect(result.gathererLanes!.length).toBe(2)
|
|
125
|
+
|
|
126
|
+
// Each lane has the right slotIndex
|
|
127
|
+
expect(result.gathererLanes![0].slotIndex).toBe(0)
|
|
128
|
+
expect(result.gathererLanes![1].slotIndex).toBe(1)
|
|
129
|
+
|
|
130
|
+
// Yields are amp-scaled and distinct
|
|
131
|
+
const caps1 = computeGathererCapabilities({strength: 300, tolerance: 200, saturation: 400}, 1)
|
|
132
|
+
const caps2 = computeGathererCapabilities({strength: 500, tolerance: 100, saturation: 300}, 1)
|
|
133
|
+
const expectedYield1 = applySlotMultiplier(caps1.yield, 100)
|
|
134
|
+
const expectedYield2 = applySlotMultiplier(caps2.yield, 100)
|
|
135
|
+
expect(result.gathererLanes![0].yield).toBe(expectedYield1)
|
|
136
|
+
expect(result.gathererLanes![1].yield).toBe(expectedYield2)
|
|
137
|
+
expect(result.gathererLanes![0].yield).not.toBe(result.gathererLanes![1].yield)
|
|
138
|
+
|
|
139
|
+
// Unscaled per-module drain and depth carried verbatim from the compute helper
|
|
140
|
+
expect(result.gathererLanes![0].drain).toBe(caps1.drain)
|
|
141
|
+
expect(result.gathererLanes![1].drain).toBe(caps2.drain)
|
|
142
|
+
expect(result.gathererLanes![0].depth).toBe(caps1.depth)
|
|
143
|
+
expect(result.gathererLanes![1].depth).toBe(caps2.depth)
|
|
144
|
+
|
|
145
|
+
// outputPct reflects the slot amp
|
|
146
|
+
expect(result.gathererLanes![0].outputPct).toBe(100)
|
|
147
|
+
expect(result.gathererLanes![1].outputPct).toBe(100)
|
|
148
|
+
|
|
149
|
+
// Legacy sum still equals sum of both lane yields
|
|
150
|
+
expect(result.gatherer).toBeDefined()
|
|
151
|
+
expect(result.gatherer!.yield).toBe(expectedYield1 + expectedYield2)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('computeEntityCapabilities emits crafterLanes alongside legacy crafter sum', () => {
|
|
155
|
+
const crafterStats = makeCrafterStats(400, 300)
|
|
156
|
+
|
|
157
|
+
const modules: InstalledModule[] = [
|
|
158
|
+
{slotIndex: 0, itemId: ITEM_CRAFTER_T1, stats: crafterStats},
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
const layout: EntitySlot[] = [{type: 'crafter', outputPct: 120, maxTier: 1}]
|
|
162
|
+
|
|
163
|
+
const result = computeEntityCapabilities({}, ITEM_EXTRACTOR_T1_PACKED, modules, layout)
|
|
164
|
+
|
|
165
|
+
expect(result.crafterLanes).toBeDefined()
|
|
166
|
+
expect(result.crafterLanes!.length).toBe(1)
|
|
167
|
+
expect(result.crafterLanes![0].slotIndex).toBe(0)
|
|
168
|
+
|
|
169
|
+
const caps = computeCrafterCapabilities({fineness: 400, conductivity: 300}, 1)
|
|
170
|
+
const expectedSpeed = applySlotMultiplier(caps.speed, 120)
|
|
171
|
+
expect(result.crafterLanes![0].speed).toBe(expectedSpeed)
|
|
172
|
+
expect(result.crafterLanes![0].drain).toBe(caps.drain)
|
|
173
|
+
expect(result.crafterLanes![0].outputPct).toBe(120)
|
|
174
|
+
|
|
175
|
+
// Legacy crafter speed equals single-lane speed
|
|
176
|
+
expect(result.crafter).toBeDefined()
|
|
177
|
+
expect(result.crafter!.speed).toBe(expectedSpeed)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('builder capabilities read canonical resonance and fineness slots', () => {
|
|
181
|
+
expect(computeBuilderCapabilities({resonance: 500, fineness: 330}, 1)).toEqual({
|
|
182
|
+
speed: 500,
|
|
183
|
+
drain: 20_000,
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('computeEntityCapabilities emits a Builder lane from resonance and fineness', () => {
|
|
188
|
+
const modules: InstalledModule[] = [
|
|
189
|
+
{slotIndex: 0, itemId: ITEM_BUILDER_T1, stats: makeBuilderStats(500, 330)},
|
|
190
|
+
]
|
|
191
|
+
const layout: EntitySlot[] = [{type: 'builder', outputPct: 80, maxTier: 1}]
|
|
192
|
+
const result = computeEntityCapabilities({}, ITEM_EXTRACTOR_T1_PACKED, modules, layout)
|
|
193
|
+
|
|
194
|
+
expect(result.builderLanes).toEqual([{slotIndex: 0, speed: 400, drain: 20_000, outputPct: 80}])
|
|
195
|
+
expect(result.builder).toEqual({speed: 400, drain: 20_000})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('computeEntityCapabilities emits loaderLanes alongside legacy loaders sum', () => {
|
|
199
|
+
const loaderStats = makeLoaderStats(600, 500)
|
|
200
|
+
|
|
201
|
+
const modules: InstalledModule[] = [{slotIndex: 0, itemId: ITEM_LOADER_T1, stats: loaderStats}]
|
|
202
|
+
|
|
203
|
+
const layout: EntitySlot[] = [{type: 'loader', outputPct: 80, maxTier: 1}]
|
|
204
|
+
|
|
205
|
+
const result = computeEntityCapabilities({}, ITEM_EXTRACTOR_T1_PACKED, modules, layout)
|
|
206
|
+
|
|
207
|
+
expect(result.loaderLanes).toBeDefined()
|
|
208
|
+
expect(result.loaderLanes!.length).toBe(1)
|
|
209
|
+
expect(result.loaderLanes![0].slotIndex).toBe(0)
|
|
210
|
+
|
|
211
|
+
const caps = computeLoaderCapabilities({insulation: 600, plasticity: 500}, 1)
|
|
212
|
+
// mass is unscaled (raw); thrust is amp-scaled
|
|
213
|
+
expect(result.loaderLanes![0].mass).toBe(caps.mass)
|
|
214
|
+
expect(result.loaderLanes![0].thrust).toBe(applySlotMultiplier(caps.thrust, 80))
|
|
215
|
+
expect(result.loaderLanes![0].outputPct).toBe(80)
|
|
216
|
+
|
|
217
|
+
// Legacy loaders.mass is total (same as single-lane raw mass here)
|
|
218
|
+
expect(result.loaders).toBeDefined()
|
|
219
|
+
expect(result.loaders!.mass).toBe(caps.mass)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('per-lane amp-scaled stats clamp to UInt16, matching the contract clamp_to_uint16', () => {
|
|
223
|
+
expect(applySlotMultiplier(60000, 200)).toBe(U16_MAX)
|
|
224
|
+
expect(applySlotMultiplier(1000, 150)).toBe(1500)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('generator capacity and recharge are denominated to milli-energy', () => {
|
|
228
|
+
const caps = computeGeneratorCapabilities({resonance: 213, reflectivity: 213}, 1)
|
|
229
|
+
expect(caps.capacity).toBe(1_406_500)
|
|
230
|
+
expect(caps.recharge).toBe(3_278)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('engine and generator capabilities use tapered quality consistently', () => {
|
|
234
|
+
const modules: InstalledModule[] = [
|
|
235
|
+
{slotIndex: 0, itemId: ITEM_ENGINE_T1, stats: encodeStats([500, 500])},
|
|
236
|
+
{slotIndex: 1, itemId: ITEM_GENERATOR_T1, stats: encodeStats([500, 500])},
|
|
237
|
+
]
|
|
238
|
+
const layout: EntitySlot[] = [
|
|
239
|
+
{type: 'engine', outputPct: 100, maxTier: 1},
|
|
240
|
+
{type: 'generator', outputPct: 100, maxTier: 1},
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
const result = computeEntityCapabilities({}, ITEM_ROUSTABOUT_T1A_PACKED, modules, layout)
|
|
244
|
+
const engines = computeEngineCapabilities({volatility: 500, thermal: 500}, 1)
|
|
245
|
+
|
|
246
|
+
expect(result.engines).toEqual({
|
|
247
|
+
thrust: engines.thrust,
|
|
248
|
+
drain: computeTravelDrain(engines.thrust, computeEffectiveModuleStat(500)),
|
|
249
|
+
})
|
|
250
|
+
expect(result.generator).toEqual(
|
|
251
|
+
computeGeneratorCapabilities({resonance: 500, reflectivity: 500}, 1)
|
|
252
|
+
)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test('gatherer/crafter/hauler drains are denominated', () => {
|
|
256
|
+
expect(computeGathererCapabilities({strength: 0, hardness: 0, saturation: 213}, 1).drain).toBe(
|
|
257
|
+
1_967_500
|
|
258
|
+
)
|
|
259
|
+
expect(computeCrafterCapabilities({fineness: 0, conductivity: 213}, 1).drain).toBe(23_546)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('gatherer depth accepts canonical tolerance and legacy recipe-labelled hardness', () => {
|
|
263
|
+
expect(computeGathererCapabilities({strength: 0, tolerance: 213, saturation: 0}, 2).depth).toBe(
|
|
264
|
+
4_343
|
|
265
|
+
)
|
|
266
|
+
expect(computeGathererCapabilities({strength: 0, hardness: 213, saturation: 0}, 1).depth).toBe(
|
|
267
|
+
1_565
|
|
268
|
+
)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe('computeGathererYield', () => {
|
|
272
|
+
test('table is the k=0.2 integer-percent curve T1..T10', () => {
|
|
273
|
+
expect([...GATHERER_YIELD_TIER_TABLE]).toEqual([
|
|
274
|
+
100, 120, 140, 160, 180, 200, 220, 240, 260, 280,
|
|
275
|
+
])
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('T1 equals the base yield 200 + str (no change)', () => {
|
|
279
|
+
expect(computeGathererYield(500, 1)).toBe(700)
|
|
280
|
+
expect(computeGathererYield(0, 1)).toBe(200)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test('T2 scales base yield by 1.2, floored', () => {
|
|
284
|
+
expect(computeGathererYield(500, 2)).toBe(840) // floor(700 * 120 / 100)
|
|
285
|
+
expect(computeGathererYield(213, 2)).toBe(495) // floor(413 * 120 / 100) = floor(495.6)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('T10 scales base yield by 2.8, floored', () => {
|
|
289
|
+
expect(computeGathererYield(500, 10)).toBe(1960) // 700 * 280 / 100
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('tier is clamped to [1, 10]', () => {
|
|
293
|
+
expect(computeGathererYield(500, 0)).toBe(700)
|
|
294
|
+
expect(computeGathererYield(500, 99)).toBe(1960)
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
describe('module tier tables', () => {
|
|
299
|
+
const ALL_TABLES = [
|
|
300
|
+
ENGINE_THRUST_TIER_PCT,
|
|
301
|
+
GENERATOR_CAPACITY_TIER_PCT,
|
|
302
|
+
GENERATOR_RECHARGE_TIER_PCT,
|
|
303
|
+
CRAFTER_SPEED_TIER_PCT,
|
|
304
|
+
BUILDER_SPEED_TIER_PCT,
|
|
305
|
+
WARP_RANGE_TIER_PCT,
|
|
306
|
+
LOADER_THRUST_TIER_PCT,
|
|
307
|
+
]
|
|
308
|
+
test('every table has 10 rows and T1 = 100', () => {
|
|
309
|
+
for (const table of ALL_TABLES) {
|
|
310
|
+
expect(table.length).toBe(10)
|
|
311
|
+
expect(table[0]).toBe(100)
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
test('moduleTierPct clamps out-of-range tiers', () => {
|
|
315
|
+
expect(moduleTierPct(ENGINE_THRUST_TIER_PCT, 0)).toBe(100)
|
|
316
|
+
expect(moduleTierPct(ENGINE_THRUST_TIER_PCT, 11)).toBe(280)
|
|
317
|
+
})
|
|
318
|
+
test('engine thrust tiers (stat 500 pre-effective)', () => {
|
|
319
|
+
expect(computeEngineThrust(500, 1)).toBe(775)
|
|
320
|
+
expect(computeEngineThrust(500, 2)).toBe(930)
|
|
321
|
+
expect(computeEngineThrust(500, 10)).toBe(2170)
|
|
322
|
+
})
|
|
323
|
+
test('generator capacity k=0.1, recharge k=0.2', () => {
|
|
324
|
+
expect(computeGeneratorCap(500, 1)).toBe(1_550_000)
|
|
325
|
+
expect(computeGeneratorCap(500, 2)).toBe(1_705_000)
|
|
326
|
+
expect(computeGeneratorCap(500, 10)).toBe(2_945_000)
|
|
327
|
+
expect(computeGeneratorRech(500, 1)).toBe(5000)
|
|
328
|
+
expect(computeGeneratorRech(500, 2)).toBe(6000)
|
|
329
|
+
expect(computeGeneratorRech(500, 10)).toBe(14000)
|
|
330
|
+
})
|
|
331
|
+
test('crafter/builder speed tiers', () => {
|
|
332
|
+
expect(computeCrafterSpeed(500, 1)).toBe(500)
|
|
333
|
+
expect(computeCrafterSpeed(500, 2)).toBe(600)
|
|
334
|
+
expect(computeBuilderSpeed(500, 2)).toBe(600)
|
|
335
|
+
expect(computeBuilderSpeed(500, 10)).toBe(1400)
|
|
336
|
+
})
|
|
337
|
+
test('warp range tiers', () => {
|
|
338
|
+
expect(computeWarpRange(500, 1)).toBe(1600)
|
|
339
|
+
expect(computeWarpRange(500, 2)).toBe(1920)
|
|
340
|
+
expect(computeWarpRange(500, 10)).toBe(4480)
|
|
341
|
+
})
|
|
342
|
+
test('loader thrust tiers floor correctly', () => {
|
|
343
|
+
expect(computeLoaderThrust(500, 1)).toBe(26)
|
|
344
|
+
expect(computeLoaderThrust(500, 2)).toBe(31)
|
|
345
|
+
expect(computeLoaderThrust(500, 10)).toBe(72)
|
|
346
|
+
expect(computeLoaderThrust(0, 2)).toBe(1)
|
|
347
|
+
})
|
|
348
|
+
})
|