@shipload/sdk 1.0.0-next.75 → 1.0.0-next.76

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.
Files changed (53) hide show
  1. package/lib/scan.js +1 -1
  2. package/lib/scan.js.map +1 -1
  3. package/lib/scan.m.js +1 -1
  4. package/lib/scan.m.js.map +1 -1
  5. package/lib/shipload.d.ts +69 -35
  6. package/lib/shipload.js +13551 -11325
  7. package/lib/shipload.js.map +1 -1
  8. package/lib/shipload.m.js +13532 -11325
  9. package/lib/shipload.m.js.map +1 -1
  10. package/lib/testing.d.ts +20 -16
  11. package/lib/testing.js +214 -48
  12. package/lib/testing.js.map +1 -1
  13. package/lib/testing.m.js +214 -48
  14. package/lib/testing.m.js.map +1 -1
  15. package/package.json +6 -6
  16. package/src/capabilities/storage.ts +45 -10
  17. package/src/contracts/server.ts +49 -41
  18. package/src/data/entities.json +367 -2
  19. package/src/data/item-ids.ts +15 -0
  20. package/src/data/items.json +90 -0
  21. package/src/data/kind-registry.json +97 -7
  22. package/src/data/metadata.ts +65 -0
  23. package/src/data/recipes-runtime.ts +1 -1
  24. package/src/data/recipes.json +1476 -10
  25. package/src/data/task-types.json +9 -0
  26. package/src/derivation/capabilities.test.ts +25 -2
  27. package/src/derivation/capabilities.ts +16 -3
  28. package/src/derivation/capacity-fn-registry.ts +8 -1
  29. package/src/derivation/reserve-regen.ts +5 -9
  30. package/src/derivation/stratum.ts +1 -1
  31. package/src/derivation/tiers.ts +1 -1
  32. package/src/derivation/upgrades.ts +5 -4
  33. package/src/index-module.ts +3 -0
  34. package/src/influence/influence.test.ts +1 -1
  35. package/src/managers/actions.ts +12 -19
  36. package/src/managers/entities.test.ts +23 -0
  37. package/src/managers/entities.ts +8 -1
  38. package/src/managers/shuttle.test.ts +67 -0
  39. package/src/managers/shuttle.ts +17 -0
  40. package/src/nft/description.ts +6 -0
  41. package/src/resolution/resolve-item.test.ts +6 -6
  42. package/src/scan/scan-wasm.base64.ts +1 -1
  43. package/src/scheduling/availability.test.ts +3 -3
  44. package/src/scheduling/availability.ts +3 -4
  45. package/src/scheduling/lanes.ts +1 -1
  46. package/src/scheduling/projection.ts +15 -13
  47. package/src/scheduling/schedule.test.ts +24 -0
  48. package/src/scheduling/schedule.ts +11 -4
  49. package/src/scheduling/task-effects.ts +3 -1
  50. package/src/scheduling/unwrap.test.ts +25 -6
  51. package/src/scheduling/unwrap.ts +19 -3
  52. package/src/travel/travel.ts +4 -0
  53. package/src/types.ts +2 -0
@@ -179,6 +179,15 @@
179
179
  "isTerminal": false,
180
180
  "capsPlan": false,
181
181
  "cargoEffectResolve": false
182
+ },
183
+ {
184
+ "type": 23,
185
+ "positionBound": false,
186
+ "mobility": false,
187
+ "consumer": false,
188
+ "isTerminal": false,
189
+ "capsPlan": false,
190
+ "cargoEffectResolve": true
182
191
  }
183
192
  ]
184
193
  }
@@ -10,7 +10,9 @@ import {
10
10
  computeLoaderCapabilities,
11
11
  computeBaseCapacity,
12
12
  computeContainerCapabilities,
13
+ computeStructureBaseCapacity,
13
14
  applyCapacityTier,
15
+ CARGO_BAY_CAPACITY_TIER_PCT,
14
16
  GATHERER_YIELD_TIER_TABLE,
15
17
  ENGINE_THRUST_TIER_PCT,
16
18
  GENERATOR_CAPACITY_TIER_PCT,
@@ -34,6 +36,8 @@ import {applySlotMultiplier, U16_MAX} from '../entities/slot-multiplier'
34
36
  import {getItem} from '../data/catalog'
35
37
  import {encodeStats} from './crafting'
36
38
  import {
39
+ ITEM_CONTAINER_T1_PACKED,
40
+ ITEM_CONTAINER_T2_PACKED,
37
41
  ITEM_EXTRACTOR_T2_PACKED,
38
42
  ITEM_FACTORY_T2_PACKED,
39
43
  ITEM_MASS_DRIVER_T1_PACKED,
@@ -74,9 +78,9 @@ function makeBuilderStats(resonance: number, fineness: number): bigint {
74
78
  return encodeStats([resonance, fineness])
75
79
  }
76
80
 
77
- test('computeBaseCapacity uses container formula for all container-class entities', () => {
81
+ test('computeBaseCapacity uses the structure formula for structure entities', () => {
78
82
  const stats = {strength: 300, hardness: 400, density: 100}
79
- const base = computeContainerCapabilities(stats).capacity
83
+ const base = computeStructureBaseCapacity(stats)
80
84
  for (const itemId of [
81
85
  ITEM_EXTRACTOR_T2_PACKED,
82
86
  ITEM_FACTORY_T2_PACKED,
@@ -89,6 +93,25 @@ test('computeBaseCapacity uses container formula for all container-class entitie
89
93
  }
90
94
  })
91
95
 
96
+ test('containers hold what a Storage module of their tier holds and weigh their packed mass', () => {
97
+ for (const [base, t1, t2, t2Mass] of [
98
+ [213, 206_600, 227_260, 27_698],
99
+ [500, 345_687, 380_255, 23_250],
100
+ ]) {
101
+ const stats = {strength: base, density: base, hardness: base}
102
+ expect(computeBaseCapacity(ITEM_CONTAINER_T1_PACKED, stats)).toBe(t1)
103
+ expect(computeBaseCapacity(ITEM_CONTAINER_T2_PACKED, stats)).toBe(t2)
104
+ expect(computeContainerCapabilities(stats, ITEM_CONTAINER_T2_PACKED).hullmass).toBe(t2Mass)
105
+ expect(t2).toBe(
106
+ Math.floor(
107
+ (computeContainerCapabilities(stats).capacity *
108
+ moduleTierPct(CARGO_BAY_CAPACITY_TIER_PCT, 2)) /
109
+ 100
110
+ )
111
+ )
112
+ }
113
+ })
114
+
92
115
  test('computeBaseCapacity uses ship-hull formula for every ship-class entity', () => {
93
116
  const stats = {strength: 300, hardness: 400, density: 100}
94
117
  const expected = computeBaseCapacity(ITEM_SHIP_T1_PACKED, stats)
@@ -322,6 +322,12 @@ const CAPACITY_FN_BY_NAME: Partial<
322
322
  depot: (stats) => computeDepotHullCapabilities(stats).capacity,
323
323
  workshop: (stats) => computeWorkshopHullCapabilities(stats).capacity,
324
324
  container: (stats) => computeContainerCapabilities(stats).capacity,
325
+ structure: computeStructureBaseCapacity,
326
+ }
327
+
328
+ export function computeStructureBaseCapacity(stats: Record<string, number>): number {
329
+ const statSum = (stats.strength ?? 0) + (stats.hardness ?? 0)
330
+ return Math.floor(220000 * 6 ** (statSum / 1998.0))
325
331
  }
326
332
 
327
333
  export function computeBaseCapacity(itemId: number, stats: Record<string, number>): number {
@@ -333,6 +339,10 @@ export function computeBaseCapacity(itemId: number, stats: Record<string, number
333
339
  const capacityFn = fnName ? CAPACITY_FN_BY_NAME[fnName] : undefined
334
340
  if (!capacityFn) return 0
335
341
  base = capacityFn(stats)
342
+ if (fnName === 'container') {
343
+ const pct = moduleTierPct(CARGO_BAY_CAPACITY_TIER_PCT, getItem(itemId).tier)
344
+ return clampUint32(Math.floor((base * pct) / 100))
345
+ }
336
346
  }
337
347
  return applyCapacityTier(base, getItem(itemId).tier)
338
348
  }
@@ -692,14 +702,17 @@ export function computeEntityCapabilities(
692
702
  return result
693
703
  }
694
704
 
695
- export function computeContainerCapabilities(stats: Record<string, number>): {
705
+ export function computeContainerCapabilities(
706
+ stats: Record<string, number>,
707
+ itemId: number = ITEM_CONTAINER_T1_PACKED
708
+ ): {
696
709
  hullmass: number
697
710
  capacity: number
698
711
  } {
699
712
  const statSum = (stats.strength ?? 0) + (stats.hardness ?? 0)
700
713
  const exponent = statSum / 1998.0
701
714
  return {
702
- hullmass: computeBaseHullmass(ITEM_CONTAINER_T1_PACKED, stats),
703
- capacity: Math.floor(220000 * 6 ** exponent),
715
+ hullmass: computeBaseHullmass(itemId, stats),
716
+ capacity: Math.floor(141000 * 6 ** exponent),
704
717
  }
705
718
  }
@@ -1,6 +1,13 @@
1
1
  import {getBaseCapacityFnName} from '../data/kind-registry'
2
2
 
3
- export const CAPACITY_FN_NAMES = ['ship', 'warehouse', 'depot', 'container', 'workshop'] as const
3
+ export const CAPACITY_FN_NAMES = [
4
+ 'ship',
5
+ 'warehouse',
6
+ 'depot',
7
+ 'container',
8
+ 'structure',
9
+ 'workshop',
10
+ ] as const
4
11
 
5
12
  export type CapacityFnName = (typeof CAPACITY_FN_NAMES)[number]
6
13
 
@@ -10,13 +10,6 @@ function toNumber(value: UInt32 | number): number {
10
10
  return typeof value === 'number' ? value : Number(value)
11
11
  }
12
12
 
13
- function slotsBetween(now: BlockTimestamp, last: BlockTimestamp): number {
14
- const nowMs = now.toMilliseconds()
15
- const lastMs = last.toMilliseconds()
16
- if (nowMs <= lastMs) return 0
17
- return Math.floor((nowMs - lastMs) / 500)
18
- }
19
-
20
13
  export function getEffectiveReserve(
21
14
  row: EffectiveReserveInput,
22
15
  now: BlockTimestamp,
@@ -27,8 +20,11 @@ export function getEffectiveReserve(
27
20
  if (remaining >= max) return max
28
21
  const epochSlots = epochSeconds * 2
29
22
  if (epochSlots === 0) return remaining
30
- const elapsed = slotsBetween(now, row.last_block)
31
- const regen = Math.floor((max * elapsed) / epochSlots)
23
+ const nowSlot = Number(now.value)
24
+ const lastSlot = Number(row.last_block.value)
25
+ if (nowSlot <= lastSlot) return remaining
26
+ const regen =
27
+ Math.floor((max * nowSlot) / epochSlots) - Math.floor((max * lastSlot) / epochSlots)
32
28
  const effective = remaining + regen
33
29
  return effective >= max ? max : effective
34
30
  }
@@ -67,7 +67,7 @@ export function deriveStratum(
67
67
  }
68
68
  }
69
69
 
70
- const tierRoll = ((bytes[18] << 8) | bytes[19]) >>> 0
70
+ const tierRoll = ((bytes[16] << 24) | (bytes[17] << 16) | (bytes[18] << 8) | bytes[19]) >>> 0
71
71
  const withinRoll = ((bytes[20] << 8) | bytes[21]) >>> 0
72
72
  const tier = rollTier(tierRoll, stratum)
73
73
  const selected = getItem(selectedItemId)
@@ -29,7 +29,7 @@ const DEEP_THRESHOLDS = {
29
29
  massive: 0.99992,
30
30
  }
31
31
 
32
- export const TIER_ROLL_MAX = 0x10000 // 65536
32
+ export const TIER_ROLL_MAX = 0x1_0000_0000
33
33
 
34
34
  function lerp(a: number, b: number, t: number): number {
35
35
  return a + (b - a) * t
@@ -3,10 +3,11 @@ import type {Recipe} from '../data/recipes-runtime'
3
3
 
4
4
  const bySource = new Map<number, Recipe[]>()
5
5
  for (const r of recipes as Recipe[]) {
6
- if (r.sourceSubclass === undefined) continue
7
- const list = bySource.get(r.sourceSubclass) ?? []
8
- list.push(r)
9
- bySource.set(r.sourceSubclass, list)
6
+ for (const source of r.sourceSubclasses ?? []) {
7
+ const list = bySource.get(source) ?? []
8
+ list.push(r)
9
+ bySource.set(source, list)
10
+ }
10
11
  }
11
12
 
12
13
  export function eligibleUpgrades(entityItemId: number): Recipe[] {
@@ -71,6 +71,7 @@ export {
71
71
  OMITTED_REASONS,
72
72
  } from './managers/shuttle'
73
73
  export type {
74
+ ShuttleMissingItem,
74
75
  ShuttleMode,
75
76
  ShuttleOption,
76
77
  ShuttleOptions,
@@ -569,6 +570,7 @@ export {
569
570
  computeStorageCapabilities,
570
571
  computeBatteryCapabilities,
571
572
  computeContainerCapabilities,
573
+ computeStructureBaseCapacity,
572
574
  computeWarpCapabilities,
573
575
  computeLauncherCapabilities,
574
576
  computeBaseCapacity,
@@ -689,6 +691,7 @@ export {
689
691
  computeBaseCapacityWarehouse,
690
692
  computeBaseCapacityDepot,
691
693
  computeBaseCapacityContainer,
694
+ computeBaseCapacityStructure,
692
695
  computeEngineThrust,
693
696
  computeEngineDrain,
694
697
  computeTravelDrain,
@@ -136,7 +136,7 @@ describe('decomposition', () => {
136
136
  for (let i = 1; i < DECOMP_REGISTRY.length; i++) {
137
137
  expect(DECOMP_REGISTRY[i].itemId).toBeGreaterThan(DECOMP_REGISTRY[i - 1].itemId)
138
138
  }
139
- expect(DECOMP_REGISTRY.length).toBe(57)
139
+ expect(DECOMP_REGISTRY.length).toBe(72)
140
140
  })
141
141
 
142
142
  test('the retired Workshop has no decomposition entry', () => {
@@ -120,15 +120,16 @@ function calcDistance(origin: CoordinatesType, destination: CoordinatesType): bi
120
120
  }
121
121
 
122
122
  function calcCargoItemMassUint32(item: ServerContract.ActionParams.Type.cargo_item): bigint {
123
- let mass = toUint32(BigInt(getItem(item.item_id).mass) * toUint32(toBigInt(item.quantity)))
123
+ let unitMass = BigInt(getItem(item.item_id).mass)
124
124
 
125
125
  for (const mod of item.modules) {
126
126
  if (mod.installed) {
127
- mass = toUint32(mass + BigInt(getItem(mod.installed.item_id).mass))
127
+ unitMass += BigInt(getItem(mod.installed.item_id).mass)
128
128
  }
129
129
  }
130
130
 
131
- return mass
131
+ const mass = unitMass * toBigInt(item.quantity)
132
+ return mass > UINT32_MAX_BIGINT ? UINT32_MAX_BIGINT : mass
132
133
  }
133
134
 
134
135
  function calcPayloadMass(items: ServerContract.ActionParams.Type.cargo_item[]): bigint {
@@ -1051,9 +1052,11 @@ export class ActionsManager extends BaseManager {
1051
1052
  })
1052
1053
  }
1053
1054
 
1054
- closeepoch(epoch: UInt64Type): Action {
1055
+ closeepoch(oracleId: NameType, epoch: UInt64Type, entropy: Checksum256Type): Action {
1055
1056
  return this.server.action('closeepoch', {
1057
+ oracle_id: Name.from(oracleId),
1056
1058
  epoch: UInt64.from(epoch),
1059
+ entropy: Checksum256.from(entropy),
1057
1060
  })
1058
1061
  }
1059
1062
 
@@ -1108,27 +1111,17 @@ export class ActionsManager extends BaseManager {
1108
1111
  entityId: UInt64Type,
1109
1112
  location: CoordinatesType,
1110
1113
  bundle: ServerContract.ActionParams.Type.cargo_item[]
1111
- ): Action {
1112
- return this.platform.action('contribute', {
1113
- game: this.server.account,
1114
+ ): Action[] {
1115
+ const args = {
1114
1116
  player: Name.from(player),
1115
1117
  entity_id: UInt64.from(entityId),
1116
1118
  x: Int64.from(location.x),
1117
1119
  y: Int64.from(location.y),
1118
1120
  bundle,
1119
- })
1120
- }
1121
-
1122
- // addcontrib asserts the citizenship rows exist, so naturalize must precede contribute.
1123
- firstContribution(
1124
- player: NameType,
1125
- entityId: UInt64Type,
1126
- location: CoordinatesType,
1127
- bundle: ServerContract.ActionParams.Type.cargo_item[]
1128
- ): Action[] {
1121
+ }
1129
1122
  return [
1130
- this.naturalize(player, location),
1131
- this.contribute(player, entityId, location, bundle),
1123
+ this.platform.action('contribute', {game: this.server.account, ...args}),
1124
+ this.server.action('contribute', args),
1132
1125
  ]
1133
1126
  }
1134
1127
 
@@ -0,0 +1,23 @@
1
+ import {describe, expect, it} from 'bun:test'
2
+ import {UInt64} from '@wharfkit/antelope'
3
+ import {EntitiesManager} from './entities'
4
+
5
+ function managerWith(getImpl: (key: unknown) => Promise<unknown>) {
6
+ const table = () => ({get: getImpl})
7
+ const ctx = {server: {table}} as never
8
+ return new EntitiesManager(ctx)
9
+ }
10
+
11
+ describe('EntitiesManager.getHullStats', () => {
12
+ it('reads the encoded stats off the raw entity row', async () => {
13
+ const m = managerWith(async () => ({stats: UInt64.from('123456789')}))
14
+ const stats = await m.getHullStats(23)
15
+ expect(stats.equals(UInt64.from('123456789'))).toBe(true)
16
+ })
17
+
18
+ it('returns zero when the chain has no row for the id', async () => {
19
+ const m = managerWith(async () => undefined)
20
+ const stats = await m.getHullStats(23)
21
+ expect(stats.equals(UInt64.from(0))).toBe(true)
22
+ })
23
+ })
@@ -1,4 +1,4 @@
1
- import {Name, type NameType, type UInt64Type} from '@wharfkit/antelope'
1
+ import {Name, UInt64, type NameType, type UInt64Type} from '@wharfkit/antelope'
2
2
  import {BaseManager} from './base'
3
3
  import {Entity} from '../entities/entity'
4
4
  import type {EntityTypeName} from '../data/kind-registry'
@@ -43,6 +43,13 @@ export class EntitiesManager extends BaseManager {
43
43
  return rows.filter((row) => wanted.equals(row.kind))
44
44
  }
45
45
 
46
+ async getHullStats(id: UInt64Type): Promise<UInt64> {
47
+ const row = (await this.server.table('entity').get(UInt64.from(id))) as
48
+ | ServerContract.Types.entity_row
49
+ | undefined
50
+ return row?.stats ?? UInt64.from(0)
51
+ }
52
+
46
53
  async getSummaries(
47
54
  owner: NameType | ServerContract.Types.player_row,
48
55
  kind?: EntityTypeName
@@ -144,6 +144,7 @@ describe('ShuttleManager', () => {
144
144
  need: null,
145
145
  cap: null,
146
146
  task_type: null,
147
+ missing: [],
147
148
  },
148
149
  }),
149
150
  ],
@@ -153,6 +154,67 @@ describe('ShuttleManager', () => {
153
154
  expect(out.options[0].blocked?.at?.getTime()).toBe(2000 * 1000)
154
155
  })
155
156
 
157
+ it('an inputs rejection lists the missing items, merged by item', async () => {
158
+ const cargo = (item_id: number, stats: number, quantity: number) => ({
159
+ item_id,
160
+ stats,
161
+ modules: [],
162
+ quantity,
163
+ entity_id: null,
164
+ })
165
+ const m = managerWithReadonly(async () => ({
166
+ options: [
167
+ rawOption({
168
+ rejection: {
169
+ reason: 'Insufficient inputs for recipe.',
170
+ at: t(2000),
171
+ until: null,
172
+ have: null,
173
+ need: null,
174
+ cap: null,
175
+ task_type: null,
176
+ missing: [cargo(101, 1, 10), cargo(201, 0, 4), cargo(101, 2, 5)],
177
+ },
178
+ }),
179
+ ],
180
+ }))
181
+ const out = await m.craft({
182
+ shipId: 5,
183
+ workshopId: 9,
184
+ recipeId: 10001,
185
+ quantity: 1,
186
+ inputs: [],
187
+ candidates: [],
188
+ })
189
+ const blocked = out.options[0].blocked!
190
+ expect(blocked.code).toBe('inputs-unavailable')
191
+ expect(blocked.missing).toEqual([
192
+ {itemId: 101, quantity: 15},
193
+ {itemId: 201, quantity: 4},
194
+ ])
195
+ })
196
+
197
+ it('a rejection with nothing missing carries no missing list', async () => {
198
+ const m = managerWithReadonly(async () => ({
199
+ options: [
200
+ rawOption({
201
+ rejection: {
202
+ reason: 'fabricator queue is full',
203
+ at: t(2000),
204
+ until: null,
205
+ have: null,
206
+ need: null,
207
+ cap: null,
208
+ task_type: null,
209
+ missing: [],
210
+ },
211
+ }),
212
+ ],
213
+ }))
214
+ const out = await m.store({shipId: 5, depotId: 9, items: [], candidates: []})
215
+ expect(out.options[0].blocked?.missing).toBeUndefined()
216
+ })
217
+
156
218
  it('a never-shuttle reason is omitted', async () => {
157
219
  const m = managerWithReadonly(async () => ({
158
220
  options: [
@@ -167,6 +229,7 @@ describe('ShuttleManager', () => {
167
229
  need: null,
168
230
  cap: null,
169
231
  task_type: null,
232
+ missing: [],
170
233
  },
171
234
  }),
172
235
  ],
@@ -184,6 +247,7 @@ describe('ShuttleManager', () => {
184
247
  need: 200,
185
248
  cap: 1000,
186
249
  task_type: null,
250
+ missing: [],
187
251
  }
188
252
  const m = managerWithReadonly(async () => ({
189
253
  options: [
@@ -206,6 +270,7 @@ describe('ShuttleManager', () => {
206
270
  need: null,
207
271
  cap: null,
208
272
  task_type: 3,
273
+ missing: [],
209
274
  }
210
275
  const m = managerWithReadonly(async () => ({
211
276
  options: [
@@ -368,6 +433,7 @@ describe('ShuttleManager', () => {
368
433
  need: null,
369
434
  cap: null,
370
435
  task_type: null,
436
+ missing: [],
371
437
  })
372
438
  const m = managerWithReadonly(async () => ({
373
439
  options: [
@@ -394,6 +460,7 @@ describe('ShuttleManager', () => {
394
460
  need: null,
395
461
  cap: null,
396
462
  task_type: null,
463
+ missing: [],
397
464
  }
398
465
  const m = managerWithReadonly(async () => ({
399
466
  options: [
@@ -42,6 +42,11 @@ export type ShuttleReasonCode =
42
42
  | 'not-ready'
43
43
  | 'unknown'
44
44
 
45
+ export interface ShuttleMissingItem {
46
+ itemId: number
47
+ quantity: number
48
+ }
49
+
45
50
  export interface ShuttleReason {
46
51
  code: ShuttleReasonCode
47
52
  reason: string
@@ -51,6 +56,7 @@ export interface ShuttleReason {
51
56
  need?: number
52
57
  cap?: number
53
58
  taskType?: number
59
+ missing?: ShuttleMissingItem[]
54
60
  }
55
61
 
56
62
  export interface ShuttleOption {
@@ -166,6 +172,16 @@ function toNum(v: {toNumber(): number} | undefined): number | undefined {
166
172
  return v === undefined ? undefined : v.toNumber()
167
173
  }
168
174
 
175
+ function mapMissing(items: ServerContract.Types.cargo_item[]): ShuttleMissingItem[] | undefined {
176
+ if (items.length === 0) return undefined
177
+ const byItem = new Map<number, number>()
178
+ for (const item of items) {
179
+ const id = item.item_id.toNumber()
180
+ byItem.set(id, (byItem.get(id) ?? 0) + item.quantity.toNumber())
181
+ }
182
+ return [...byItem].map(([itemId, quantity]) => ({itemId, quantity}))
183
+ }
184
+
169
185
  function mapReason(r: ServerContract.Types.shuttle_rejection): ShuttleReason {
170
186
  return {
171
187
  code: shuttleReasonCode(r.reason),
@@ -176,6 +192,7 @@ function mapReason(r: ServerContract.Types.shuttle_rejection): ShuttleReason {
176
192
  need: toNum(r.need),
177
193
  cap: toNum(r.cap),
178
194
  taskType: toNum(r.task_type),
195
+ missing: mapMissing(r.missing),
179
196
  }
180
197
  }
181
198
 
@@ -71,6 +71,11 @@ export function computeBaseCapacityShip(stats: bigint): number {
71
71
  }
72
72
 
73
73
  export function computeBaseCapacityContainer(stats: bigint): number {
74
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
75
+ return Math.floor(141_000 * 6 ** (s / 1998))
76
+ }
77
+
78
+ export function computeBaseCapacityStructure(stats: bigint): number {
74
79
  const s = decodeStat(stats, 0) + decodeStat(stats, 2)
75
80
  return Math.floor(220_000 * 6 ** (s / 1998))
76
81
  }
@@ -95,6 +100,7 @@ const CAPACITY_FN_BY_NAME: Record<CapacityFnName, (stats: bigint) => number> = {
95
100
  warehouse: computeBaseCapacityWarehouse,
96
101
  workshop: computeBaseCapacityWorkshop,
97
102
  container: computeBaseCapacityContainer,
103
+ structure: computeBaseCapacityStructure,
98
104
  depot: computeBaseCapacityDepot,
99
105
  }
100
106
 
@@ -4,7 +4,7 @@ import {resolveItem} from './resolve-item'
4
4
  import {encodeStats} from '../derivation/crafting'
5
5
  import {
6
6
  computeBaseHullmass,
7
- computeContainerCapabilities,
7
+ computeStructureBaseCapacity,
8
8
  applyCapacityTier,
9
9
  } from '../derivation/capabilities'
10
10
  import {getItem} from '../data/catalog'
@@ -20,24 +20,24 @@ function hullStats(strength: number, density: number, hardness: number): UInt64
20
20
  return UInt64.from(encodeStats([strength, density, hardness]).toString())
21
21
  }
22
22
 
23
- const CONTAINER_ENTITIES = [
23
+ const STRUCTURE_ENTITIES = [
24
24
  ['factory', ITEM_FACTORY_T2_PACKED],
25
25
  ['extractor', ITEM_EXTRACTOR_T2_PACKED],
26
26
  ['mass driver', ITEM_MASS_DRIVER_T1_PACKED],
27
27
  ['mass catcher', ITEM_MASS_CATCHER_T1_PACKED],
28
28
  ] as const
29
29
 
30
- for (const [label, itemId] of CONTAINER_ENTITIES) {
31
- test(`resolveItem resolves ${label} hull capacity via container formula`, () => {
30
+ for (const [label, itemId] of STRUCTURE_ENTITIES) {
31
+ test(`resolveItem resolves ${label} hull capacity via structure formula`, () => {
32
32
  const stats = hullStats(300, 100, 400)
33
33
  const resolved = resolveItem(UInt16.from(itemId), stats)
34
34
  const hull = resolved.attributes?.find((g) => g.capability === 'Hull')
35
35
  const capacity = hull?.attributes.find((a) => a.label === 'Capacity')?.value
36
- const base = computeContainerCapabilities({
36
+ const base = computeStructureBaseCapacity({
37
37
  strength: 300,
38
38
  hardness: 400,
39
39
  density: 100,
40
- }).capacity
40
+ })
41
41
  expect(capacity).toBe(applyCapacityTier(base, getItem(itemId).tier))
42
42
  })
43
43
  }