@shipload/sdk 1.0.0-next.5 → 1.0.0-next.50
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 +2881 -1149
- package/lib/shipload.js +13031 -5511
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +12807 -5460
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +1028 -0
- package/lib/testing.js +4336 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +4330 -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 +6 -7
- package/src/capabilities/gathering.test.ts +29 -0
- package/src/capabilities/gathering.ts +37 -19
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +22 -0
- package/src/capabilities/storage.ts +16 -1
- package/src/contracts/platform.ts +231 -3
- package/src/contracts/server.ts +1160 -520
- 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 +31 -16
- package/src/data/capability-formulas.ts +21 -14
- package/src/data/catalog.ts +0 -5
- package/src/data/colors.ts +13 -47
- package/src/data/entities.json +154 -10
- package/src/data/item-ids.ts +31 -12
- package/src/data/items.json +192 -74
- package/src/data/kind-registry.json +131 -0
- package/src/data/kind-registry.ts +172 -0
- package/src/data/metadata.ts +166 -48
- package/src/data/recipes-runtime.ts +5 -23
- package/src/data/recipes.json +1003 -163
- package/src/derivation/build-methods.test.ts +13 -0
- package/src/derivation/build-methods.ts +48 -0
- package/src/derivation/capabilities.test.ts +151 -0
- package/src/derivation/capabilities.ts +527 -0
- package/src/derivation/capability-mappings.ts +58 -16
- package/src/derivation/crafting.ts +36 -33
- package/src/derivation/index.ts +28 -2
- package/src/derivation/recipe-usage.test.ts +81 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +98 -19
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +56 -0
- package/src/derivation/stars.test.ts +67 -0
- package/src/derivation/stars.ts +25 -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 +141 -0
- package/src/entities/entity.ts +98 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +131 -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 +290 -34
- package/src/managers/actions.ts +602 -105
- 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 +412 -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/flatten-gather-plan.test.ts +80 -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 +336 -0
- package/src/nft/description.ts +104 -36
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +242 -0
- package/src/planner/planner.test.ts +325 -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 +74 -47
- 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.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 +210 -107
- package/src/scheduling/schedule.ts +240 -117
- 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 +23 -0
- package/src/travel/route-planner.ts +277 -0
- package/src/travel/travel.ts +148 -117
- 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 +36 -14
- 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 +36 -24
- 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
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import {UInt8, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
|
|
2
|
+
import {expect, test, describe} from 'bun:test'
|
|
3
|
+
import {ServerContract} from '../contracts'
|
|
4
|
+
import {
|
|
5
|
+
planParallelTransfer,
|
|
6
|
+
type GatherPlanEntity,
|
|
7
|
+
gatherEnergyCost,
|
|
8
|
+
splitCost,
|
|
9
|
+
maxQtyForCharge,
|
|
10
|
+
buildGatherPlan,
|
|
11
|
+
} from './index'
|
|
12
|
+
|
|
13
|
+
function gathererLane(
|
|
14
|
+
slotIndex: number,
|
|
15
|
+
yieldVal: number,
|
|
16
|
+
drain: number,
|
|
17
|
+
depth: number
|
|
18
|
+
): ServerContract.Types.gatherer_lane {
|
|
19
|
+
return ServerContract.Types.gatherer_lane.from({
|
|
20
|
+
slot_index: UInt8.from(slotIndex),
|
|
21
|
+
yield: UInt16.from(yieldVal),
|
|
22
|
+
drain: UInt32.from(drain),
|
|
23
|
+
depth: UInt16.from(depth),
|
|
24
|
+
output_pct: UInt16.from(100),
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loaderLane(
|
|
29
|
+
slotIndex: number,
|
|
30
|
+
mass: number,
|
|
31
|
+
thrust: number
|
|
32
|
+
): ServerContract.Types.loader_lane {
|
|
33
|
+
return ServerContract.Types.loader_lane.from({
|
|
34
|
+
slot_index: UInt8.from(slotIndex),
|
|
35
|
+
mass: UInt32.from(mass),
|
|
36
|
+
thrust: UInt16.from(thrust),
|
|
37
|
+
output_pct: UInt16.from(100),
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function energyStats(capacity: number, recharge: number): ServerContract.Types.energy_stats {
|
|
42
|
+
return ServerContract.Types.energy_stats.from({
|
|
43
|
+
capacity: UInt32.from(capacity),
|
|
44
|
+
recharge: UInt32.from(recharge),
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface EntityOverrides {
|
|
49
|
+
gatherer_lanes?: ServerContract.Types.gatherer_lane[]
|
|
50
|
+
loader_lanes?: ServerContract.Types.loader_lane[]
|
|
51
|
+
generator?: ServerContract.Types.energy_stats
|
|
52
|
+
energy?: number
|
|
53
|
+
lanes?: ServerContract.Types.lane[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function entity(overrides: EntityOverrides = {}): GatherPlanEntity {
|
|
57
|
+
return {
|
|
58
|
+
gatherer_lanes: overrides.gatherer_lanes ?? [],
|
|
59
|
+
loader_lanes: overrides.loader_lanes ?? [],
|
|
60
|
+
generator: overrides.generator,
|
|
61
|
+
energy: overrides.energy !== undefined ? UInt16.from(overrides.energy) : undefined,
|
|
62
|
+
lanes: overrides.lanes ?? [],
|
|
63
|
+
coordinates: ServerContract.Types.coordinates.from({x: 0, y: 0}),
|
|
64
|
+
cargo: [],
|
|
65
|
+
cargomass: UInt32.from(0),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const NOW = new Date('2026-06-21T00:00:00.000Z')
|
|
70
|
+
|
|
71
|
+
describe('planParallelTransfer', () => {
|
|
72
|
+
test('two loader lanes: quantities proportional to thrust, sums to target', () => {
|
|
73
|
+
const THRUST1 = 100
|
|
74
|
+
const THRUST2 = 200
|
|
75
|
+
const QUANTITY = 90
|
|
76
|
+
|
|
77
|
+
const e = entity({
|
|
78
|
+
loader_lanes: [loaderLane(0, 500, THRUST1), loaderLane(1, 500, THRUST2)],
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const plan = planParallelTransfer(e, {quantity: QUANTITY})
|
|
82
|
+
|
|
83
|
+
expect(plan).toHaveLength(2)
|
|
84
|
+
expect(plan.reduce((s, p) => s + p.quantity, 0)).toBe(QUANTITY)
|
|
85
|
+
|
|
86
|
+
const q1 = plan.find((p) => p.slot === 0)!.quantity
|
|
87
|
+
const q2 = plan.find((p) => p.slot === 1)!.quantity
|
|
88
|
+
expect(q2 / q1).toBeCloseTo(THRUST2 / THRUST1, 0)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('no loader lanes: returns empty plan', () => {
|
|
92
|
+
const plan = planParallelTransfer(entity({loader_lanes: []}), {quantity: 10})
|
|
93
|
+
expect(plan).toHaveLength(0)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('thrust=0 loader lane (no-loader/mobility case): returns empty plan', () => {
|
|
97
|
+
const plan = planParallelTransfer(entity({loader_lanes: [loaderLane(0, 500, 0)]}), {
|
|
98
|
+
quantity: 10,
|
|
99
|
+
})
|
|
100
|
+
expect(plan).toHaveLength(0)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test("'max' target: each loader lane gets >= 1 unit", () => {
|
|
104
|
+
const e = entity({
|
|
105
|
+
loader_lanes: [loaderLane(0, 500, 100), loaderLane(1, 500, 200)],
|
|
106
|
+
})
|
|
107
|
+
const plan = planParallelTransfer(e, 'max')
|
|
108
|
+
expect(plan.length).toBeGreaterThan(0)
|
|
109
|
+
for (const entry of plan) {
|
|
110
|
+
expect(entry.quantity).toBeGreaterThanOrEqual(1)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('cost helpers', () => {
|
|
116
|
+
const STRATUM = 100
|
|
117
|
+
const ITEM_MASS = 1000
|
|
118
|
+
const RICHNESS = 500
|
|
119
|
+
|
|
120
|
+
test('gatherEnergyCost is 0 for non-positive quantity and positive otherwise', () => {
|
|
121
|
+
const lane = gathererLane(0, 700, 1250, 5000)
|
|
122
|
+
expect(gatherEnergyCost(lane, 0, STRATUM, ITEM_MASS, RICHNESS)).toBe(0)
|
|
123
|
+
expect(gatherEnergyCost(lane, 100, STRATUM, ITEM_MASS, RICHNESS)).toBeGreaterThan(0)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('splitCost across two lanes is monotonic non-decreasing in quantity', () => {
|
|
127
|
+
const reaching = [gathererLane(0, 700, 1250, 5000), gathererLane(1, 700, 1250, 5000)]
|
|
128
|
+
const c100 = splitCost(reaching, 100, STRATUM, ITEM_MASS, RICHNESS)
|
|
129
|
+
const c200 = splitCost(reaching, 200, STRATUM, ITEM_MASS, RICHNESS)
|
|
130
|
+
expect(c200).toBeGreaterThanOrEqual(c100)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('maxQtyForCharge returns hi when the whole batch fits the charge', () => {
|
|
134
|
+
const reaching = [gathererLane(0, 700, 1250, 5000)]
|
|
135
|
+
// capacity huge → the full 500 fits
|
|
136
|
+
expect(maxQtyForCharge(reaching, 500, 1_000_000, STRATUM, ITEM_MASS, RICHNESS)).toBe(500)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('maxQtyForCharge finds the boundary: result fits, result+1 does not', () => {
|
|
140
|
+
const reaching = [gathererLane(0, 700, 1250, 5000)]
|
|
141
|
+
const capacity = 40
|
|
142
|
+
const q = maxQtyForCharge(reaching, 100_000, capacity, STRATUM, ITEM_MASS, RICHNESS)
|
|
143
|
+
expect(splitCost(reaching, q, STRATUM, ITEM_MASS, RICHNESS)).toBeLessThanOrEqual(capacity)
|
|
144
|
+
expect(splitCost(reaching, q + 1, STRATUM, ITEM_MASS, RICHNESS)).toBeGreaterThan(capacity)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('buildGatherPlan', () => {
|
|
149
|
+
const STRATUM = 100
|
|
150
|
+
const OPTS = {
|
|
151
|
+
richness: 500,
|
|
152
|
+
itemMass: 1000,
|
|
153
|
+
holdRoom: 1_000_000,
|
|
154
|
+
reserveRemaining: 1_000_000,
|
|
155
|
+
now: NOW,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test('single cycle when the target fits one charge and energy is full', () => {
|
|
159
|
+
const e = entity({
|
|
160
|
+
gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
|
|
161
|
+
generator: energyStats(1200, 2),
|
|
162
|
+
energy: 1200,
|
|
163
|
+
})
|
|
164
|
+
const plan = buildGatherPlan(e, STRATUM, {quantity: 100}, OPTS)
|
|
165
|
+
expect(plan.cycleCount).toBe(1)
|
|
166
|
+
expect(plan.totalOre).toBe(100)
|
|
167
|
+
expect(plan.cycles[0].rechargeBefore).toBe(false)
|
|
168
|
+
expect(plan.cycles[0].limpets).toHaveLength(1)
|
|
169
|
+
expect(plan.cycles[0].limpets[0].quantity).toBe(100)
|
|
170
|
+
expect(plan.cap).toBe('requested')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('two limpets split one charge proportional to yield, finish within 1s of each other', () => {
|
|
174
|
+
const e = entity({
|
|
175
|
+
gatherer_lanes: [gathererLane(0, 200, 500, 5000), gathererLane(1, 400, 500, 5000)],
|
|
176
|
+
generator: energyStats(60_000, 2),
|
|
177
|
+
energy: 60_000,
|
|
178
|
+
})
|
|
179
|
+
const plan = buildGatherPlan(e, STRATUM, {quantity: 60}, OPTS)
|
|
180
|
+
expect(plan.cycleCount).toBe(1)
|
|
181
|
+
const [a, b] = plan.cycles[0].limpets
|
|
182
|
+
expect(a.quantity + b.quantity).toBe(60)
|
|
183
|
+
expect(b.quantity).toBeGreaterThan(a.quantity) // higher yield gets more
|
|
184
|
+
expect(Math.abs(a.durationSeconds - b.durationSeconds)).toBeLessThanOrEqual(2)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('multi-cycle: fills beyond one charge; totalOre equals fill target; each cycle fits the charge', () => {
|
|
188
|
+
const CAP = 40
|
|
189
|
+
const e = entity({
|
|
190
|
+
gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
|
|
191
|
+
generator: energyStats(CAP, 2),
|
|
192
|
+
energy: CAP,
|
|
193
|
+
})
|
|
194
|
+
const plan = buildGatherPlan(e, STRATUM, {quantity: 500}, OPTS)
|
|
195
|
+
expect(plan.cycleCount).toBeGreaterThan(1)
|
|
196
|
+
expect(plan.totalOre).toBe(500)
|
|
197
|
+
// every cycle's summed gather cost fits one full charge
|
|
198
|
+
for (const c of plan.cycles) {
|
|
199
|
+
const cost = splitCost(
|
|
200
|
+
e.gatherer_lanes,
|
|
201
|
+
c.batchOre,
|
|
202
|
+
STRATUM,
|
|
203
|
+
OPTS.itemMass,
|
|
204
|
+
OPTS.richness
|
|
205
|
+
)
|
|
206
|
+
expect(cost).toBeLessThanOrEqual(CAP)
|
|
207
|
+
}
|
|
208
|
+
// cycles after the first recharge to full
|
|
209
|
+
expect(plan.cycles.slice(1).every((c) => c.rechargeBefore)).toBe(true)
|
|
210
|
+
// limpet quantities across all cycles sum to the target
|
|
211
|
+
const gathered = plan.cycles.flatMap((c) => c.limpets).reduce((s, l) => s + l.quantity, 0)
|
|
212
|
+
expect(gathered).toBe(500)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('cap reasons: hold binds, reserve binds, requested binds', () => {
|
|
216
|
+
const e = entity({
|
|
217
|
+
gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
|
|
218
|
+
generator: energyStats(60_000, 2),
|
|
219
|
+
energy: 60_000,
|
|
220
|
+
})
|
|
221
|
+
expect(
|
|
222
|
+
buildGatherPlan(e, STRATUM, 'max', {...OPTS, holdRoom: 300, reserveRemaining: 9999}).cap
|
|
223
|
+
).toBe('hold')
|
|
224
|
+
expect(
|
|
225
|
+
buildGatherPlan(e, STRATUM, 'max', {...OPTS, holdRoom: 9999, reserveRemaining: 200}).cap
|
|
226
|
+
).toBe('reserve')
|
|
227
|
+
expect(buildGatherPlan(e, STRATUM, {quantity: 50}, OPTS).cap).toBe('requested')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('total ETA sums per-cycle recharge + gather seconds', () => {
|
|
231
|
+
const CAP = 40
|
|
232
|
+
const e = entity({
|
|
233
|
+
gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
|
|
234
|
+
generator: energyStats(CAP, 2),
|
|
235
|
+
energy: CAP,
|
|
236
|
+
})
|
|
237
|
+
const plan = buildGatherPlan(e, STRATUM, {quantity: 500}, OPTS)
|
|
238
|
+
const expected = plan.cycles.reduce((s, c) => s + c.rechargeSeconds + c.gatherSeconds, 0)
|
|
239
|
+
expect(plan.totalSeconds).toBe(expected)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('excludes limpets that cannot reach the stratum and warns', () => {
|
|
243
|
+
const e = entity({
|
|
244
|
+
gatherer_lanes: [
|
|
245
|
+
gathererLane(0, 700, 1250, 5000), // reaches 100
|
|
246
|
+
gathererLane(1, 700, 1250, 50), // does not reach 100
|
|
247
|
+
],
|
|
248
|
+
generator: energyStats(60_000, 2),
|
|
249
|
+
energy: 60_000,
|
|
250
|
+
})
|
|
251
|
+
const plan = buildGatherPlan(e, STRATUM, {quantity: 100}, OPTS)
|
|
252
|
+
expect(plan.cycles[0].limpets).toHaveLength(1)
|
|
253
|
+
expect(plan.cycles[0].limpets[0].slot).toBe(0)
|
|
254
|
+
expect(plan.warnings.some((w) => w.includes("can't reach"))).toBe(true)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('reports structured limpet reach counts (all reaching)', () => {
|
|
258
|
+
const e = entity({
|
|
259
|
+
gatherer_lanes: [gathererLane(0, 700, 1250, 5000), gathererLane(1, 700, 1250, 5000)],
|
|
260
|
+
generator: energyStats(60_000, 2),
|
|
261
|
+
energy: 60_000,
|
|
262
|
+
})
|
|
263
|
+
const plan = buildGatherPlan(e, STRATUM, {quantity: 50}, OPTS)
|
|
264
|
+
expect(plan.totalLimpets).toBe(2)
|
|
265
|
+
expect(plan.reachingCount).toBe(2)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('reachingCount excludes limpets that cannot reach the stratum', () => {
|
|
269
|
+
const e = entity({
|
|
270
|
+
gatherer_lanes: [
|
|
271
|
+
gathererLane(0, 700, 1250, 5000), // reaches 100
|
|
272
|
+
gathererLane(1, 700, 1250, 50), // does not reach 100
|
|
273
|
+
],
|
|
274
|
+
generator: energyStats(60_000, 2),
|
|
275
|
+
energy: 60_000,
|
|
276
|
+
})
|
|
277
|
+
const plan = buildGatherPlan(e, STRATUM, {quantity: 50}, OPTS)
|
|
278
|
+
expect(plan.totalLimpets).toBe(2)
|
|
279
|
+
expect(plan.reachingCount).toBe(1)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('single-limpet ship still fills across cycles', () => {
|
|
283
|
+
const CAP = 40
|
|
284
|
+
const e = entity({
|
|
285
|
+
gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
|
|
286
|
+
generator: energyStats(CAP, 2),
|
|
287
|
+
energy: CAP,
|
|
288
|
+
})
|
|
289
|
+
const plan = buildGatherPlan(e, STRATUM, {quantity: 300}, OPTS)
|
|
290
|
+
expect(plan.cycles.every((c) => c.limpets.length === 1)).toBe(true)
|
|
291
|
+
expect(plan.totalOre).toBe(300)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test('throws when no gatherer reaches the stratum', () => {
|
|
295
|
+
const e = entity({
|
|
296
|
+
gatherer_lanes: [gathererLane(0, 700, 1250, 50)],
|
|
297
|
+
generator: energyStats(1200, 2),
|
|
298
|
+
energy: 1200,
|
|
299
|
+
})
|
|
300
|
+
expect(() => buildGatherPlan(e, STRATUM, 'max', OPTS)).toThrow('no gatherer reaches')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('zero fill target yields an empty plan (no cycles)', () => {
|
|
304
|
+
const e = entity({
|
|
305
|
+
gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
|
|
306
|
+
generator: energyStats(1200, 2),
|
|
307
|
+
energy: 1200,
|
|
308
|
+
})
|
|
309
|
+
const plan = buildGatherPlan(e, STRATUM, 'max', {...OPTS, holdRoom: 0})
|
|
310
|
+
expect(plan.cycleCount).toBe(0)
|
|
311
|
+
expect(plan.totalOre).toBe(0)
|
|
312
|
+
expect(plan.cap).toBe('hold')
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
test('first cycle skips recharge when current energy already covers the small batch', () => {
|
|
316
|
+
const e = entity({
|
|
317
|
+
gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
|
|
318
|
+
generator: energyStats(60_000, 2),
|
|
319
|
+
energy: 60_000, // full
|
|
320
|
+
})
|
|
321
|
+
const plan = buildGatherPlan(e, STRATUM, {quantity: 50}, OPTS)
|
|
322
|
+
expect(plan.cycles[0].rechargeBefore).toBe(false)
|
|
323
|
+
expect(plan.cycles[0].rechargeSeconds).toBe(0)
|
|
324
|
+
})
|
|
325
|
+
})
|
|
@@ -20,6 +20,7 @@ export interface ModuleDescription {
|
|
|
20
20
|
export interface RenderDescriptionOptions {
|
|
21
21
|
translate?: (id: string, fallback: string) => string
|
|
22
22
|
formatNumber?: (n: number) => string
|
|
23
|
+
formatParam?: (paramName: string, value: number, descId: string) => string | undefined
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
interface TemplateSpec {
|
|
@@ -27,6 +28,7 @@ interface TemplateSpec {
|
|
|
27
28
|
template: string
|
|
28
29
|
params: readonly [string, string][]
|
|
29
30
|
highlightKeys: readonly string[]
|
|
31
|
+
derive?: Record<string, (get: (label: string) => number) => number>
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const TEMPLATES: Record<string, TemplateSpec> = {
|
|
@@ -53,43 +55,48 @@ const TEMPLATES: Record<string, TemplateSpec> = {
|
|
|
53
55
|
gatherer: {
|
|
54
56
|
id: 'module.gatherer.description',
|
|
55
57
|
template:
|
|
56
|
-
'mines resources at {yield}
|
|
58
|
+
'mines resources at {yield} yield to a max depth of {depth} while draining {drain} energy per minute',
|
|
57
59
|
params: [
|
|
58
60
|
['yield', 'Yield'],
|
|
59
|
-
['drain', 'Drain'],
|
|
60
61
|
['depth', 'Depth'],
|
|
61
|
-
['speed', 'Speed'],
|
|
62
62
|
],
|
|
63
|
-
highlightKeys: ['yield', 'depth', '
|
|
63
|
+
highlightKeys: ['yield', 'depth', 'drain'],
|
|
64
|
+
derive: {
|
|
65
|
+
drain: (get) => Math.round((get('Drain') / 10000) * 60 * 10) / 10,
|
|
66
|
+
},
|
|
64
67
|
},
|
|
65
|
-
|
|
66
|
-
id: 'module.
|
|
67
|
-
template:
|
|
68
|
-
'{quantity} loader that generates {thrust} thrust with a weight of {mass} per unit',
|
|
68
|
+
loading: {
|
|
69
|
+
id: 'module.loading.description',
|
|
70
|
+
template: 'generates {thrust} thrust with a weight of {mass} per unit',
|
|
69
71
|
params: [
|
|
70
|
-
['quantity', 'Quantity'],
|
|
71
72
|
['thrust', 'Thrust'],
|
|
72
73
|
['mass', 'Mass'],
|
|
73
74
|
],
|
|
74
|
-
highlightKeys: ['
|
|
75
|
+
highlightKeys: ['thrust', 'mass'],
|
|
75
76
|
},
|
|
76
|
-
|
|
77
|
-
id: 'module.
|
|
78
|
-
template: 'manufactures items at {speed} speed while draining {drain} energy per
|
|
79
|
-
params: [
|
|
80
|
-
['speed', 'Speed'],
|
|
81
|
-
['drain', 'Drain'],
|
|
82
|
-
],
|
|
77
|
+
crafting: {
|
|
78
|
+
id: 'module.crafting.description',
|
|
79
|
+
template: 'manufactures items at {speed} speed while draining {drain} energy per minute',
|
|
80
|
+
params: [['speed', 'Speed']],
|
|
83
81
|
highlightKeys: ['speed', 'drain'],
|
|
82
|
+
derive: {
|
|
83
|
+
drain: (get) => Math.round(((get('Drain') * get('Speed')) / 150000) * 60 * 10) / 10,
|
|
84
|
+
},
|
|
84
85
|
},
|
|
85
86
|
storage: {
|
|
86
87
|
id: 'module.storage.description',
|
|
87
|
-
template: '
|
|
88
|
-
params: [['
|
|
89
|
-
highlightKeys: ['
|
|
88
|
+
template: 'adds {capacity} cargo capacity',
|
|
89
|
+
params: [['capacity', 'Cargo Capacity']],
|
|
90
|
+
highlightKeys: ['capacity'],
|
|
91
|
+
},
|
|
92
|
+
energy: {
|
|
93
|
+
id: 'module.energy-capacity.description',
|
|
94
|
+
template: 'adds {capacity} energy capacity',
|
|
95
|
+
params: [['capacity', 'Energy Capacity']],
|
|
96
|
+
highlightKeys: ['capacity'],
|
|
90
97
|
},
|
|
91
|
-
|
|
92
|
-
id: 'module.
|
|
98
|
+
hauling: {
|
|
99
|
+
id: 'module.hauling.description',
|
|
93
100
|
template:
|
|
94
101
|
'locks onto up to {capacity} targets at {efficiency} efficiency while draining {drain} energy per distance travelled per target',
|
|
95
102
|
params: [
|
|
@@ -111,6 +118,12 @@ export function describeModule(input: CapabilityInput): ModuleDescription | null
|
|
|
111
118
|
const attr = input.attributes.find((a) => a.label === attrLabel)
|
|
112
119
|
if (attr) params[paramName] = attr.value
|
|
113
120
|
}
|
|
121
|
+
if (spec.derive) {
|
|
122
|
+
const get = (label: string) => input.attributes.find((a) => a.label === label)?.value ?? 0
|
|
123
|
+
for (const [paramName, fn] of Object.entries(spec.derive)) {
|
|
124
|
+
params[paramName] = fn(get)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
114
127
|
return {
|
|
115
128
|
id: spec.id,
|
|
116
129
|
template: spec.template,
|
|
@@ -127,8 +140,10 @@ export function describeModuleForItem(resolved: ResolvedItem): ModuleDescription
|
|
|
127
140
|
}
|
|
128
141
|
|
|
129
142
|
export function describeModuleForSlot(slot: ResolvedModuleSlot): ModuleDescription | null {
|
|
130
|
-
if (!slot.installed || !slot.
|
|
131
|
-
|
|
143
|
+
if (!slot.installed || !slot.attributes) return null
|
|
144
|
+
const capability = slot.capability ?? slot.name
|
|
145
|
+
if (!capability) return null
|
|
146
|
+
return describeModule({capability, attributes: slot.attributes})
|
|
132
147
|
}
|
|
133
148
|
|
|
134
149
|
export function renderDescription(
|
|
@@ -153,7 +168,10 @@ export function renderDescription(
|
|
|
153
168
|
if (raw === undefined) {
|
|
154
169
|
spans.push({text: `{${paramName}}`})
|
|
155
170
|
} else {
|
|
156
|
-
const formatted =
|
|
171
|
+
const formatted =
|
|
172
|
+
typeof raw === 'number'
|
|
173
|
+
? (options?.formatParam?.(paramName, raw, desc.id) ?? formatNumber(raw))
|
|
174
|
+
: raw
|
|
157
175
|
const highlight = (desc.highlightKeys as readonly string[]).includes(paramName)
|
|
158
176
|
spans.push(highlight ? {text: formatted, highlight: true} : {text: formatted})
|
|
159
177
|
}
|
|
@@ -1,22 +1,50 @@
|
|
|
1
1
|
import type {ResolvedItem} from './resolve-item'
|
|
2
2
|
import type {ResourceCategory} from '../types'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CATEGORY_LABELS,
|
|
5
|
+
RESOURCE_TIER_ADJECTIVES,
|
|
6
|
+
COMPONENT_TIER_PREFIXES,
|
|
7
|
+
MODULE_TIER_PREFIXES,
|
|
8
|
+
} from '../types'
|
|
4
9
|
import {formatMass as defaultFormatMass} from '../format'
|
|
5
10
|
|
|
6
|
-
|
|
7
|
-
itemType: 'resource' | 'component' | 'module' | 'entity' | string
|
|
11
|
+
interface DisplayNameInputCommon {
|
|
8
12
|
tier: number
|
|
9
13
|
category?: ResourceCategory
|
|
10
14
|
name: string
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
export type DisplayNameInput =
|
|
18
|
+
| (DisplayNameInputCommon & {itemType: 'resource' | 'component' | 'module' | 'entity' | string})
|
|
19
|
+
| (DisplayNameInputCommon & {type: string})
|
|
20
|
+
|
|
21
|
+
function itemTypeOf(item: DisplayNameInput): string {
|
|
22
|
+
return 'itemType' in item ? item.itemType : item.type
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function tierPrefix(item: DisplayNameInput): string | null {
|
|
26
|
+
const t = itemTypeOf(item)
|
|
27
|
+
if (t === 'resource') return RESOURCE_TIER_ADJECTIVES[item.tier] ?? null
|
|
28
|
+
if (t === 'component') return COMPONENT_TIER_PREFIXES[item.tier] ?? null
|
|
29
|
+
if (t === 'module') return MODULE_TIER_PREFIXES[item.tier] ?? null
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rootName(item: DisplayNameInput): string {
|
|
34
|
+
if (itemTypeOf(item) !== 'resource') return item.name
|
|
35
|
+
return item.category ? CATEGORY_LABELS[item.category] : 'Resource'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Tier-free display name: includes the resource tier adjective / component-module
|
|
39
|
+
// prefix, but no "(T#)" suffix. Use this when the tier is shown separately.
|
|
40
|
+
export function baseName(item: DisplayNameInput): string {
|
|
41
|
+
const prefix = tierPrefix(item)
|
|
42
|
+
const root = rootName(item)
|
|
43
|
+
return prefix ? `${prefix} ${root}` : root
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function displayName(item: DisplayNameInput): string {
|
|
47
|
+
return `${baseName(item)} (T${item.tier})`
|
|
20
48
|
}
|
|
21
49
|
|
|
22
50
|
export interface DescribeOptions {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {expect, test} from 'bun:test'
|
|
2
|
+
import {UInt16, UInt64} from '@wharfkit/antelope'
|
|
3
|
+
import {resolveItem} from './resolve-item'
|
|
4
|
+
import {encodeStats} from '../derivation/crafting'
|
|
5
|
+
import {computeBaseHullmass, computeContainerCapabilities} from '../derivation/capabilities'
|
|
6
|
+
import {
|
|
7
|
+
ITEM_EXTRACTOR_T1_PACKED,
|
|
8
|
+
ITEM_FACTORY_T1_PACKED,
|
|
9
|
+
ITEM_HUB_T1_PACKED,
|
|
10
|
+
ITEM_MASS_DRIVER_T1_PACKED,
|
|
11
|
+
ITEM_MASS_CATCHER_T1_PACKED,
|
|
12
|
+
} from '../data/item-ids'
|
|
13
|
+
|
|
14
|
+
function hullStats(strength: number, density: number, hardness: number): UInt64 {
|
|
15
|
+
return UInt64.from(encodeStats([strength, density, hardness]).toString())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CONTAINER_ENTITIES = [
|
|
19
|
+
['factory', ITEM_FACTORY_T1_PACKED],
|
|
20
|
+
['extractor', ITEM_EXTRACTOR_T1_PACKED],
|
|
21
|
+
['mass driver', ITEM_MASS_DRIVER_T1_PACKED],
|
|
22
|
+
['mass catcher', ITEM_MASS_CATCHER_T1_PACKED],
|
|
23
|
+
] as const
|
|
24
|
+
|
|
25
|
+
for (const [label, itemId] of CONTAINER_ENTITIES) {
|
|
26
|
+
test(`resolveItem resolves ${label} hull capacity via container formula`, () => {
|
|
27
|
+
const stats = hullStats(300, 100, 400)
|
|
28
|
+
const resolved = resolveItem(UInt16.from(itemId), stats)
|
|
29
|
+
const hull = resolved.attributes?.find((g) => g.capability === 'Hull')
|
|
30
|
+
const capacity = hull?.attributes.find((a) => a.label === 'Capacity')?.value
|
|
31
|
+
const expected = computeContainerCapabilities({
|
|
32
|
+
strength: 300,
|
|
33
|
+
hardness: 400,
|
|
34
|
+
density: 100,
|
|
35
|
+
}).capacity
|
|
36
|
+
expect(capacity).toBe(expected)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test('resolveItem resolves the station hub with hullmass and zero cargo capacity', () => {
|
|
41
|
+
const stats = hullStats(300, 100, 400)
|
|
42
|
+
const resolved = resolveItem(UInt16.from(ITEM_HUB_T1_PACKED), stats)
|
|
43
|
+
const hull = resolved.attributes?.find((g) => g.capability === 'Hull')
|
|
44
|
+
expect(hull?.attributes.find((a) => a.label === 'Mass')?.value).toBe(
|
|
45
|
+
computeBaseHullmass({density: 100})
|
|
46
|
+
)
|
|
47
|
+
expect(hull?.attributes.find((a) => a.label === 'Capacity')?.value).toBe(0)
|
|
48
|
+
})
|