@shipload/sdk 1.0.0-next.2 → 1.0.0-next.4

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.
@@ -0,0 +1,68 @@
1
+ export interface SlotConsumer {
2
+ capability: string
3
+ attribute: string
4
+ }
5
+
6
+ export type SlotConsumerKind =
7
+ | 'engine'
8
+ | 'generator'
9
+ | 'gatherer'
10
+ | 'loader'
11
+ | 'crafter'
12
+ | 'storage'
13
+ | 'hauler'
14
+ | 'warp'
15
+ | 'ship-t1'
16
+ | 'container-t1'
17
+ | 'warehouse-t1'
18
+ | 'container-t2'
19
+
20
+ const ENTITY_HULL_SLOTS: Record<number, SlotConsumer> = {
21
+ 0: {capability: 'Storage', attribute: 'capacity'},
22
+ 1: {capability: 'Hull', attribute: 'mass'},
23
+ 2: {capability: 'Storage', attribute: 'capacity'},
24
+ 3: {capability: 'Storage', attribute: 'capacity'},
25
+ }
26
+
27
+ export const SLOT_FORMULAS: Record<SlotConsumerKind, Record<number, SlotConsumer>> = {
28
+ engine: {
29
+ 0: {capability: 'Movement', attribute: 'thrust'},
30
+ 1: {capability: 'Movement', attribute: 'drain'},
31
+ },
32
+ generator: {
33
+ 0: {capability: 'Energy', attribute: 'capacity'},
34
+ 1: {capability: 'Energy', attribute: 'recharge'},
35
+ },
36
+ gatherer: {
37
+ 0: {capability: 'Gathering', attribute: 'yield'},
38
+ 1: {capability: 'Gathering', attribute: 'depth'},
39
+ 3: {capability: 'Gathering', attribute: 'drain'},
40
+ 4: {capability: 'Gathering', attribute: 'speed'},
41
+ },
42
+ loader: {
43
+ 0: {capability: 'Loader', attribute: 'mass'},
44
+ 1: {capability: 'Loader', attribute: 'thrust'},
45
+ },
46
+ crafter: {
47
+ 0: {capability: 'Crafter', attribute: 'speed'},
48
+ 1: {capability: 'Crafter', attribute: 'drain'},
49
+ },
50
+ storage: {
51
+ 0: {capability: 'Storage', attribute: 'bonus'},
52
+ 1: {capability: 'Storage', attribute: 'bonus'},
53
+ 2: {capability: 'Storage', attribute: 'bonus'},
54
+ 3: {capability: 'Storage', attribute: 'bonus'},
55
+ },
56
+ hauler: {
57
+ 0: {capability: 'Hauler', attribute: 'capacity'},
58
+ 1: {capability: 'Hauler', attribute: 'efficiency'},
59
+ 2: {capability: 'Hauler', attribute: 'drain'},
60
+ },
61
+ warp: {
62
+ 0: {capability: 'Warp', attribute: 'range'},
63
+ },
64
+ 'ship-t1': ENTITY_HULL_SLOTS,
65
+ 'container-t1': ENTITY_HULL_SLOTS,
66
+ 'warehouse-t1': ENTITY_HULL_SLOTS,
67
+ 'container-t2': ENTITY_HULL_SLOTS,
68
+ }
@@ -75,6 +75,7 @@ export const itemAbbreviations: Record<number, string> = {
75
75
  10103: 'LD',
76
76
  10104: 'MF',
77
77
  10105: 'ST',
78
+ 10107: 'WP',
78
79
  10200: 'CT',
79
80
  10201: 'SH',
80
81
  10202: 'WH',
@@ -1,4 +1,4 @@
1
- // Synced from game/build/catalog via `make sync-catalog` — do not edit.
1
+ // Generated by scripts/build-catalog.ts — do not edit.
2
2
 
3
3
  export const ITEM_ORE_T1 = 101
4
4
  export const ITEM_ORE_T2 = 102
@@ -67,6 +67,7 @@ export const ITEM_LOADER_T1 = 10103
67
67
  export const ITEM_CRAFTER_T1 = 10104
68
68
  export const ITEM_STORAGE_T1 = 10105
69
69
  export const ITEM_HAULER_T1 = 10106
70
+ export const ITEM_WARP_T1 = 10107
70
71
  export const ITEM_CONTAINER_T1_PACKED = 10200
71
72
  export const ITEM_SHIP_T1_PACKED = 10201
72
73
  export const ITEM_WAREHOUSE_T1_PACKED = 10202
@@ -213,6 +213,13 @@
213
213
  "tier": 1,
214
214
  "subtype": "hauler"
215
215
  },
216
+ {
217
+ "id": 10107,
218
+ "mass": 180000,
219
+ "type": "module",
220
+ "tier": 1,
221
+ "subtype": "warp"
222
+ },
216
223
  {
217
224
  "id": 10200,
218
225
  "mass": 80000,
@@ -157,6 +157,12 @@ export const itemMetadata: Record<number, ItemMetadata> = {
157
157
  'Projects a haul beam to lock onto and transport containers or warehouses through group travel.',
158
158
  color: '#4ADBFF',
159
159
  },
160
+ 10107: {
161
+ name: 'Warp',
162
+ description:
163
+ 'Folds local space-time around the hull, projecting the ship across vast distances in a single discharge of the entire energy reserve.',
164
+ color: '#9be4ff',
165
+ },
160
166
 
161
167
  // === Entities (packed, T1) ===
162
168
  10200: {
@@ -393,12 +393,7 @@
393
393
  ]
394
394
  },
395
395
  {
396
- "sources": [
397
- {
398
- "inputIndex": 1,
399
- "statIndex": 1
400
- }
401
- ]
396
+ "sources": []
402
397
  },
403
398
  {
404
399
  "sources": [
@@ -436,8 +431,8 @@
436
431
  {
437
432
  "sources": [
438
433
  {
439
- "inputIndex": 0,
440
- "statIndex": 0
434
+ "inputIndex": 1,
435
+ "statIndex": 1
441
436
  }
442
437
  ]
443
438
  },
@@ -552,7 +547,7 @@
552
547
  "sources": [
553
548
  {
554
549
  "inputIndex": 0,
555
- "statIndex": 0
550
+ "statIndex": 1
556
551
  }
557
552
  ]
558
553
  },
@@ -568,12 +563,36 @@
568
563
  "sources": [
569
564
  {
570
565
  "inputIndex": 0,
571
- "statIndex": 1
566
+ "statIndex": 0
572
567
  }
573
568
  ]
574
569
  },
570
+ {
571
+ "sources": []
572
+ }
573
+ ],
574
+ "blendWeights": []
575
+ },
576
+ {
577
+ "outputItemId": 10107,
578
+ "outputMass": 180000,
579
+ "inputs": [
580
+ {
581
+ "itemId": 10010,
582
+ "quantity": 6
583
+ },
584
+ {
585
+ "itemId": 10009,
586
+ "quantity": 4
587
+ }
588
+ ],
589
+ "statSlots": [
575
590
  {
576
591
  "sources": [
592
+ {
593
+ "inputIndex": 0,
594
+ "statIndex": 1
595
+ },
577
596
  {
578
597
  "inputIndex": 1,
579
598
  "statIndex": 1
@@ -581,7 +600,10 @@
581
600
  ]
582
601
  }
583
602
  ],
584
- "blendWeights": []
603
+ "blendWeights": [
604
+ 1,
605
+ 1
606
+ ]
585
607
  },
586
608
  {
587
609
  "outputItemId": 10200,
@@ -0,0 +1,120 @@
1
+ import {SLOT_FORMULAS, type SlotConsumerKind} from '../data/capability-formulas'
2
+ import {getStatDefinitions, type StatDefinition} from './stats'
3
+ import {
4
+ getRecipe,
5
+ type Recipe,
6
+ type RecipeInput,
7
+ type RecipeInputCategory,
8
+ } from '../data/recipes-runtime'
9
+ import {
10
+ ITEM_ENGINE_T1,
11
+ ITEM_GENERATOR_T1,
12
+ ITEM_GATHERER_T1,
13
+ ITEM_LOADER_T1,
14
+ ITEM_CRAFTER_T1,
15
+ ITEM_STORAGE_T1,
16
+ ITEM_HAULER_T1,
17
+ ITEM_WARP_T1,
18
+ ITEM_SHIP_T1_PACKED,
19
+ ITEM_CONTAINER_T1_PACKED,
20
+ ITEM_WAREHOUSE_T1_PACKED,
21
+ ITEM_CONTAINER_T2_PACKED,
22
+ } from '../data/item-ids'
23
+ import type {StatMapping} from '../data/capabilities'
24
+
25
+ export const KIND_TO_ITEM_ID: Record<SlotConsumerKind, number> = {
26
+ engine: ITEM_ENGINE_T1,
27
+ generator: ITEM_GENERATOR_T1,
28
+ gatherer: ITEM_GATHERER_T1,
29
+ loader: ITEM_LOADER_T1,
30
+ crafter: ITEM_CRAFTER_T1,
31
+ storage: ITEM_STORAGE_T1,
32
+ hauler: ITEM_HAULER_T1,
33
+ warp: ITEM_WARP_T1,
34
+ 'ship-t1': ITEM_SHIP_T1_PACKED,
35
+ 'container-t1': ITEM_CONTAINER_T1_PACKED,
36
+ 'warehouse-t1': ITEM_WAREHOUSE_T1_PACKED,
37
+ 'container-t2': ITEM_CONTAINER_T2_PACKED,
38
+ }
39
+
40
+ function isCategoryInput(input: RecipeInput): input is RecipeInputCategory {
41
+ return 'category' in input
42
+ }
43
+
44
+ /**
45
+ * Walk a recipe's slot source down to the raw category stat that ultimately
46
+ * lands in that slot. Returns the StatDefinition or undefined if the trace
47
+ * dead-ends (unknown sub-component, missing slot, etc.).
48
+ *
49
+ * Multi-source sub-slots collapse to `sources[0]`; top-level multi-source slots
50
+ * are expanded by the caller (`deriveStatMappings`).
51
+ */
52
+ function traceToRawCategoryStat(
53
+ recipe: Recipe,
54
+ source: {inputIndex: number; statIndex: number},
55
+ visited: Set<number> = new Set()
56
+ ): StatDefinition | undefined {
57
+ const input = recipe.inputs[source.inputIndex]
58
+ if (!input) return undefined
59
+ if (isCategoryInput(input)) {
60
+ const defs = getStatDefinitions(input.category)
61
+ return defs[source.statIndex]
62
+ }
63
+ if (visited.has(input.itemId)) return undefined
64
+ const subRecipe = getRecipe(input.itemId)
65
+ if (!subRecipe) return undefined
66
+ const subSlot = subRecipe.statSlots[source.statIndex]
67
+ if (!subSlot) return undefined
68
+ const subSource = subSlot.sources[0]
69
+ if (!subSource) return undefined
70
+ const nextVisited = new Set(visited)
71
+ nextVisited.add(input.itemId)
72
+ return traceToRawCategoryStat(subRecipe, subSource, nextVisited)
73
+ }
74
+
75
+ let cached: StatMapping[] | undefined
76
+
77
+ export function deriveStatMappings(): StatMapping[] {
78
+ if (cached) return cached
79
+ const out: StatMapping[] = []
80
+ const seen = new Set<string>()
81
+ for (const [kind, slots] of Object.entries(SLOT_FORMULAS) as [
82
+ SlotConsumerKind,
83
+ Record<number, {capability: string; attribute: string}>,
84
+ ][]) {
85
+ const itemId = KIND_TO_ITEM_ID[kind]
86
+ const recipe = getRecipe(itemId)
87
+ if (!recipe) continue
88
+ for (const [slotIdxStr, consumer] of Object.entries(slots)) {
89
+ const slotIdx = Number(slotIdxStr)
90
+ const slot = recipe.statSlots[slotIdx]
91
+ if (!slot) continue
92
+ for (const source of slot.sources) {
93
+ const stat = traceToRawCategoryStat(recipe, source)
94
+ if (!stat) continue
95
+ const key = `${stat.label}|${consumer.capability}|${consumer.attribute}`
96
+ if (seen.has(key)) continue
97
+ seen.add(key)
98
+ out.push({
99
+ stat: stat.label,
100
+ capability: consumer.capability,
101
+ attribute: consumer.attribute,
102
+ })
103
+ }
104
+ }
105
+ }
106
+ cached = out
107
+ return out
108
+ }
109
+
110
+ export function getStatMappings(): StatMapping[] {
111
+ return deriveStatMappings()
112
+ }
113
+
114
+ export function getStatMappingsForStat(stat: string): StatMapping[] {
115
+ return deriveStatMappings().filter((m) => m.stat === stat)
116
+ }
117
+
118
+ export function getStatMappingsForCapability(capability: string): StatMapping[] {
119
+ return deriveStatMappings().filter((m) => m.capability === capability)
120
+ }
@@ -75,10 +75,10 @@ export function computeContainerCapabilities(stats: Record<string, number>): {
75
75
  hullmass: number
76
76
  capacity: number
77
77
  } {
78
- const density = stats['density'] ?? 500
79
- const strength = stats['strength'] ?? 500
80
- const hardness = stats['hardness'] ?? 500
81
- const saturation = stats['saturation'] ?? 500
78
+ const density = stats.density
79
+ const strength = stats.strength
80
+ const hardness = stats.hardness
81
+ const saturation = stats.saturation
82
82
 
83
83
  const hullmass = 25000 + 75 * density
84
84
 
@@ -93,10 +93,10 @@ export function computeContainerT2Capabilities(stats: Record<string, number>): {
93
93
  hullmass: number
94
94
  capacity: number
95
95
  } {
96
- const strength = stats['strength'] ?? 0
97
- const density = stats['density'] ?? 0
98
- const hardness = stats['hardness'] ?? 0
99
- const saturation = stats['saturation'] ?? 0
96
+ const strength = stats.strength
97
+ const density = stats.density
98
+ const hardness = stats.hardness
99
+ const saturation = stats.saturation
100
100
 
101
101
  const hullmass = 20000 + 50 * density
102
102
 
@@ -13,10 +13,10 @@ export function computeShipHullCapabilities(stats: Record<string, number>): {
13
13
  hullmass: number
14
14
  capacity: number
15
15
  } {
16
- const density = stats.density ?? 500
17
- const strength = stats.strength ?? 500
18
- const hardness = stats.hardness ?? 500
19
- const saturation = stats.saturation ?? 500
16
+ const density = stats.density
17
+ const strength = stats.strength
18
+ const hardness = stats.hardness
19
+ const saturation = stats.saturation
20
20
 
21
21
  const hullmass = 25000 + 75 * density
22
22
  const statSum = strength + hardness + saturation
@@ -30,8 +30,8 @@ export function computeEngineCapabilities(stats: Record<string, number>): {
30
30
  thrust: number
31
31
  drain: number
32
32
  } {
33
- const vol = stats.volatility ?? 500
34
- const thm = stats.thermal ?? 500
33
+ const vol = stats.volatility
34
+ const thm = stats.thermal
35
35
 
36
36
  return {
37
37
  thrust: 400 + Math.floor((vol * 3) / 4),
@@ -43,12 +43,12 @@ export function computeGeneratorCapabilities(stats: Record<string, number>): {
43
43
  capacity: number
44
44
  recharge: number
45
45
  } {
46
- const res = stats.resonance ?? 500
47
- const ref = stats.reflectivity ?? 500
46
+ const com = stats.composition
47
+ const fin = stats.fineness
48
48
 
49
49
  return {
50
- capacity: 300 + Math.floor(res / 6),
51
- recharge: 1 + Math.floor((ref * 3) / 1000),
50
+ capacity: 300 + Math.floor(com / 6),
51
+ recharge: 1 + Math.floor((fin * 3) / 1000),
52
52
  }
53
53
  }
54
54
 
@@ -58,10 +58,10 @@ export function computeGathererCapabilities(stats: Record<string, number>): {
58
58
  depth: number
59
59
  speed: number
60
60
  } {
61
- const str = stats.strength ?? 500
62
- const con = stats.conductivity ?? 500
63
- const ref = stats.reflectivity ?? 500
64
- const tol = stats.tolerance ?? 500
61
+ const str = stats.strength
62
+ const con = stats.conductivity
63
+ const ref = stats.reflectivity
64
+ const tol = stats.tolerance
65
65
 
66
66
  return {
67
67
  yield: 200 + str,
@@ -76,12 +76,12 @@ export function computeLoaderCapabilities(stats: Record<string, number>): {
76
76
  thrust: number
77
77
  quantity: number
78
78
  } {
79
- const hrd = stats.hardness ?? 500
80
- const pla = stats.plasticity ?? 500
79
+ const insulation = stats.insulation
80
+ const plasticity = stats.plasticity
81
81
 
82
82
  return {
83
- mass: Math.max(200, 2000 - Math.floor(hrd * 2)),
84
- thrust: 1 + Math.floor(pla / 500),
83
+ mass: Math.max(200, 2000 - Math.floor(insulation * 2)),
84
+ thrust: 1 + Math.floor(plasticity / 500),
85
85
  quantity: 1,
86
86
  }
87
87
  }
@@ -90,12 +90,12 @@ export function computeCrafterCapabilities(stats: Record<string, number>): {
90
90
  speed: number
91
91
  drain: number
92
92
  } {
93
- const rea = stats.reactivity ?? 500
94
- const com = stats.composition ?? 500
93
+ const rea = stats.reactivity
94
+ const fin = stats.fineness
95
95
 
96
96
  return {
97
97
  speed: 100 + Math.floor((rea * 4) / 5),
98
- drain: Math.max(5, 30 - Math.floor(com / 33)),
98
+ drain: Math.max(5, 30 - Math.floor(fin / 33)),
99
99
  }
100
100
  }
101
101
 
@@ -104,14 +104,14 @@ export function computeHaulerCapabilities(stats: Record<string, number>): {
104
104
  efficiency: number
105
105
  drain: number
106
106
  } {
107
- const res = stats.resonance ?? 500
108
- const con = stats.conductivity ?? 500
109
- const ref = stats.reflectivity ?? 500
107
+ const fineness = stats.fineness
108
+ const conductivity = stats.conductivity
109
+ const composition = stats.composition
110
110
 
111
111
  return {
112
- capacity: Math.max(1, 1 + Math.floor(res / 400)),
113
- efficiency: 2000 + con * 6,
114
- drain: Math.max(3, 15 - Math.floor(ref / 80)),
112
+ capacity: Math.max(1, 1 + Math.floor(fineness / 400)),
113
+ efficiency: 2000 + conductivity * 6,
114
+ drain: Math.max(3, 15 - Math.floor(composition / 80)),
115
115
  }
116
116
  }
117
117
 
@@ -121,11 +121,12 @@ export function computeStorageCapabilities(
121
121
  ): {
122
122
  capacityBonus: number
123
123
  } {
124
- const strength = stats.strength ?? 500
125
- const hardness = stats.hardness ?? 500
126
- const saturation = stats.saturation ?? 500
124
+ const strength = stats.strength
125
+ const density = stats.density
126
+ const hardness = stats.hardness
127
+ const saturation = stats.saturation
127
128
 
128
- const statSum = strength + hardness + saturation
129
+ const statSum = strength + density + hardness + saturation
129
130
  const capacityBonus = Math.floor(
130
131
  (baseCapacity * (10 + Math.floor((statSum * 10) / 2997))) / 100
131
132
  )
@@ -137,10 +138,10 @@ export function computeWarehouseHullCapabilities(stats: Record<string, number>):
137
138
  hullmass: number
138
139
  capacity: number
139
140
  } {
140
- const density = stats.density ?? 500
141
- const strength = stats.strength ?? 500
142
- const hardness = stats.hardness ?? 500
143
- const saturation = stats.saturation ?? 500
141
+ const density = stats.density
142
+ const strength = stats.strength
143
+ const hardness = stats.hardness
144
+ const saturation = stats.saturation
144
145
 
145
146
  const hullmass = 25000 + 75 * density
146
147
  const statSum = strength + hardness + saturation
@@ -202,14 +202,19 @@ export type {PlanetSubtypeInfo} from './data/locations'
202
202
  export {
203
203
  capabilityNames,
204
204
  capabilityAttributes,
205
- statMappings,
206
205
  isInvertedAttribute,
207
206
  getCapabilityAttributes,
207
+ } from './data/capabilities'
208
+ export type {CapabilityAttribute, StatMapping} from './data/capabilities'
209
+
210
+ export {
211
+ deriveStatMappings,
208
212
  getStatMappings,
209
213
  getStatMappingsForStat,
210
214
  getStatMappingsForCapability,
211
- } from './data/capabilities'
212
- export type {CapabilityAttribute, StatMapping} from './data/capabilities'
215
+ } from './derivation/capability-mappings'
216
+ export {SLOT_FORMULAS} from './data/capability-formulas'
217
+ export type {SlotConsumer, SlotConsumerKind} from './data/capability-formulas'
213
218
 
214
219
  export {
215
220
  encodeStats,
@@ -302,6 +307,7 @@ export {
302
307
  computeLoaderThrust,
303
308
  computeCrafterSpeed,
304
309
  computeCrafterDrain,
310
+ computeWarpRange,
305
311
  } from './nft/description'
306
312
 
307
313
  export {
@@ -6,6 +6,7 @@ import {
6
6
  MODULE_GENERATOR,
7
7
  MODULE_LOADER,
8
8
  MODULE_STORAGE,
9
+ MODULE_WARP,
9
10
  } from '../capabilities/modules'
10
11
  import {
11
12
  ITEM_CONTAINER_T1_PACKED,
@@ -18,6 +19,7 @@ import {
18
19
  ITEM_SHIP_T1_PACKED,
19
20
  ITEM_STORAGE_T1,
20
21
  ITEM_WAREHOUSE_T1_PACKED,
22
+ ITEM_WARP_T1,
21
23
  } from '../data/item-ids'
22
24
  import {decodeStat} from '../derivation/crafting'
23
25
 
@@ -42,17 +44,18 @@ export function computeBaseCapacityWarehouse(stats: bigint): number {
42
44
 
43
45
  export const computeEngineThrust = (vol: number): number => 400 + idiv(vol * 3, 4)
44
46
  export const computeEngineDrain = (thm: number): number => Math.max(30, 50 - idiv(thm, 70))
45
- export const computeGeneratorCap = (res: number): number => 300 + idiv(res, 6)
46
- export const computeGeneratorRech = (ref: number): number => 1 + idiv(ref * 3, 1000)
47
+ export const computeGeneratorCap = (com: number): number => 300 + idiv(com, 6)
48
+ export const computeGeneratorRech = (fin: number): number => 1 + idiv(fin * 3, 1000)
47
49
  export const computeGathererYield = (str: number): number => 200 + str
48
50
  export const computeGathererDrain = (con: number): number =>
49
51
  Math.max(250, 1250 - idiv(con * 25, 20))
50
52
  export const computeGathererDepth = (tol: number): number => 200 + idiv(tol * 3, 2)
51
53
  export const computeGathererSpeed = (ref: number): number => 100 + idiv(ref * 4, 5)
52
- export const computeLoaderMass = (fin: number): number => Math.max(200, 2000 - fin * 2)
54
+ export const computeLoaderMass = (ins: number): number => Math.max(200, 2000 - ins * 2)
53
55
  export const computeLoaderThrust = (pla: number): number => 1 + idiv(pla, 500)
54
56
  export const computeCrafterSpeed = (rea: number): number => 100 + idiv(rea * 4, 5)
55
- export const computeCrafterDrain = (com: number): number => Math.max(5, 30 - idiv(com, 33))
57
+ export const computeCrafterDrain = (fin: number): number => Math.max(5, 30 - idiv(fin, 33))
58
+ export const computeWarpRange = (stat: number): number => 100 + stat * 3
56
59
 
57
60
  export function entityDisplayName(itemId: number): string {
58
61
  switch (itemId) {
@@ -83,6 +86,8 @@ export function moduleDisplayName(itemId: number): string {
83
86
  return 'Crafter'
84
87
  case ITEM_STORAGE_T1:
85
88
  return 'Storage'
89
+ case ITEM_WARP_T1:
90
+ return 'Warp'
86
91
  default:
87
92
  return 'Module'
88
93
  }
@@ -142,6 +147,11 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
142
147
  out += ` +${pct}% capacity`
143
148
  break
144
149
  }
150
+ case MODULE_WARP: {
151
+ const stat = decodeStat(stats, 0)
152
+ out += ` Range ${computeWarpRange(stat)}`
153
+ break
154
+ }
145
155
  }
146
156
  return out
147
157
  }
@@ -223,10 +223,11 @@ function computeCapabilityGroup(
223
223
  }
224
224
  }
225
225
  case MODULE_STORAGE: {
226
- const str = stats.strength ?? 500
227
- const hrd = stats.hardness ?? 500
228
- const sat = stats.saturation ?? 500
229
- const statSum = str + hrd + sat
226
+ const str = stats.strength
227
+ const den = stats.density
228
+ const hrd = stats.hardness
229
+ const sat = stats.saturation
230
+ const statSum = str + den + hrd + sat
230
231
  const pct = 10 + Math.floor((statSum * 10) / 2997)
231
232
  return {capability: 'Storage', attributes: [{label: 'Capacity Bonus', value: pct}]}
232
233
  }