@shipload/sdk 1.0.0-next.3 → 1.0.0-next.5

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.
@@ -11,7 +11,7 @@ export interface EntityMetadata {
11
11
  }
12
12
 
13
13
  export const itemMetadata: Record<number, ItemMetadata> = {
14
- // === Resources (raw) ===
14
+ // === Resources / Ore ===
15
15
  101: {name: 'Ore', description: 'Crude metallic ore.', color: '#C26D3F'},
16
16
  102: {name: 'Ore', description: 'Refined metallic ore with improved purity.', color: '#C26D3F'},
17
17
  103: {
@@ -19,6 +19,15 @@ export const itemMetadata: Record<number, ItemMetadata> = {
19
19
  description: 'High-grade metallic ore with exceptional density.',
20
20
  color: '#C26D3F',
21
21
  },
22
+ 104: {name: 'Ore', description: '', color: '#C26D3F'},
23
+ 105: {name: 'Ore', description: '', color: '#C26D3F'},
24
+ 106: {name: 'Ore', description: '', color: '#C26D3F'},
25
+ 107: {name: 'Ore', description: '', color: '#C26D3F'},
26
+ 108: {name: 'Ore', description: '', color: '#C26D3F'},
27
+ 109: {name: 'Ore', description: '', color: '#C26D3F'},
28
+ 110: {name: 'Ore', description: '', color: '#C26D3F'},
29
+
30
+ // === Resources / Crystal ===
22
31
  201: {name: 'Crystal', description: 'Raw resonant crystal.', color: '#4ADBFF'},
23
32
  202: {
24
33
  name: 'Crystal',
@@ -30,6 +39,15 @@ export const itemMetadata: Record<number, ItemMetadata> = {
30
39
  description: 'High-grade resonant crystal with exceptional purity.',
31
40
  color: '#4ADBFF',
32
41
  },
42
+ 204: {name: 'Crystal', description: '', color: '#4ADBFF'},
43
+ 205: {name: 'Crystal', description: '', color: '#4ADBFF'},
44
+ 206: {name: 'Crystal', description: '', color: '#4ADBFF'},
45
+ 207: {name: 'Crystal', description: '', color: '#4ADBFF'},
46
+ 208: {name: 'Crystal', description: '', color: '#4ADBFF'},
47
+ 209: {name: 'Crystal', description: '', color: '#4ADBFF'},
48
+ 210: {name: 'Crystal', description: '', color: '#4ADBFF'},
49
+
50
+ // === Resources / Gas ===
33
51
  301: {name: 'Gas', description: 'Raw volatile gas.', color: '#B8E4A0'},
34
52
  302: {
35
53
  name: 'Gas',
@@ -41,6 +59,15 @@ export const itemMetadata: Record<number, ItemMetadata> = {
41
59
  description: 'High-grade volatile gas with exceptional energy density.',
42
60
  color: '#B8E4A0',
43
61
  },
62
+ 304: {name: 'Gas', description: '', color: '#B8E4A0'},
63
+ 305: {name: 'Gas', description: '', color: '#B8E4A0'},
64
+ 306: {name: 'Gas', description: '', color: '#B8E4A0'},
65
+ 307: {name: 'Gas', description: '', color: '#B8E4A0'},
66
+ 308: {name: 'Gas', description: '', color: '#B8E4A0'},
67
+ 309: {name: 'Gas', description: '', color: '#B8E4A0'},
68
+ 310: {name: 'Gas', description: '', color: '#B8E4A0'},
69
+
70
+ // === Resources / Regolith ===
44
71
  401: {name: 'Regolith', description: 'Crude regolith dust.', color: '#C4A57B'},
45
72
  402: {
46
73
  name: 'Regolith',
@@ -52,6 +79,15 @@ export const itemMetadata: Record<number, ItemMetadata> = {
52
79
  description: 'High-grade regolith with exceptional uniformity.',
53
80
  color: '#C4A57B',
54
81
  },
82
+ 404: {name: 'Regolith', description: '', color: '#C4A57B'},
83
+ 405: {name: 'Regolith', description: '', color: '#C4A57B'},
84
+ 406: {name: 'Regolith', description: '', color: '#C4A57B'},
85
+ 407: {name: 'Regolith', description: '', color: '#C4A57B'},
86
+ 408: {name: 'Regolith', description: '', color: '#C4A57B'},
87
+ 409: {name: 'Regolith', description: '', color: '#C4A57B'},
88
+ 410: {name: 'Regolith', description: '', color: '#C4A57B'},
89
+
90
+ // === Resources / Biomass ===
55
91
  501: {name: 'Biomass', description: 'Crude organic biomass.', color: '#5A8B3E'},
56
92
  502: {
57
93
  name: 'Biomass',
@@ -63,6 +99,13 @@ export const itemMetadata: Record<number, ItemMetadata> = {
63
99
  description: 'High-grade biomass with exceptional saturation.',
64
100
  color: '#5A8B3E',
65
101
  },
102
+ 504: {name: 'Biomass', description: '', color: '#5A8B3E'},
103
+ 505: {name: 'Biomass', description: '', color: '#5A8B3E'},
104
+ 506: {name: 'Biomass', description: '', color: '#5A8B3E'},
105
+ 507: {name: 'Biomass', description: '', color: '#5A8B3E'},
106
+ 508: {name: 'Biomass', description: '', color: '#5A8B3E'},
107
+ 509: {name: 'Biomass', description: '', color: '#5A8B3E'},
108
+ 510: {name: 'Biomass', description: '', color: '#5A8B3E'},
66
109
 
67
110
  // === Components (T1) ===
68
111
  10001: {
@@ -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,17 +563,12 @@
568
563
  "sources": [
569
564
  {
570
565
  "inputIndex": 0,
571
- "statIndex": 1
566
+ "statIndex": 0
572
567
  }
573
568
  ]
574
569
  },
575
570
  {
576
- "sources": [
577
- {
578
- "inputIndex": 1,
579
- "statIndex": 1
580
- }
581
- ]
571
+ "sources": []
582
572
  }
583
573
  ],
584
574
  "blendWeights": []
@@ -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
+ }
@@ -1,10 +1,15 @@
1
1
  import {getItem} from '../data/catalog'
2
2
 
3
3
  export const DEPTH_THRESHOLD_T1 = 0
4
- export const DEPTH_THRESHOLD_T2 = 2000
5
- export const DEPTH_THRESHOLD_T3 = 10000
6
- export const DEPTH_THRESHOLD_T4 = 30000
7
- export const DEPTH_THRESHOLD_T5 = 55000
4
+ export const DEPTH_THRESHOLD_T2 = 1500
5
+ export const DEPTH_THRESHOLD_T3 = 5000
6
+ export const DEPTH_THRESHOLD_T4 = 12000
7
+ export const DEPTH_THRESHOLD_T5 = 22000
8
+ export const DEPTH_THRESHOLD_T6 = 32000
9
+ export const DEPTH_THRESHOLD_T7 = 42000
10
+ export const DEPTH_THRESHOLD_T8 = 50000
11
+ export const DEPTH_THRESHOLD_T9 = 57000
12
+ export const DEPTH_THRESHOLD_T10 = 63000
8
13
 
9
14
  export const LOCATION_MIN_DEPTH = 500
10
15
  export const LOCATION_MAX_DEPTH = 65535
@@ -18,19 +23,22 @@ export const PLANET_SUBTYPE_ICY = 3
18
23
  export const PLANET_SUBTYPE_OCEAN = 4
19
24
  export const PLANET_SUBTYPE_INDUSTRIAL = 5
20
25
 
26
+ const DEPTH_THRESHOLD_TABLE = [
27
+ DEPTH_THRESHOLD_T1,
28
+ DEPTH_THRESHOLD_T2,
29
+ DEPTH_THRESHOLD_T3,
30
+ DEPTH_THRESHOLD_T4,
31
+ DEPTH_THRESHOLD_T5,
32
+ DEPTH_THRESHOLD_T6,
33
+ DEPTH_THRESHOLD_T7,
34
+ DEPTH_THRESHOLD_T8,
35
+ DEPTH_THRESHOLD_T9,
36
+ DEPTH_THRESHOLD_T10,
37
+ ]
38
+
21
39
  export function getDepthThreshold(tier: number): number {
22
- switch (tier) {
23
- case 1:
24
- return DEPTH_THRESHOLD_T1
25
- case 2:
26
- return DEPTH_THRESHOLD_T2
27
- case 3:
28
- return DEPTH_THRESHOLD_T3
29
- case 4:
30
- return DEPTH_THRESHOLD_T4
31
- default:
32
- return DEPTH_THRESHOLD_T5
33
- }
40
+ if (tier < 1 || tier > 10) return 65535
41
+ return DEPTH_THRESHOLD_TABLE[tier - 1]
34
42
  }
35
43
 
36
44
  export function getResourceTier(itemId: number): number {
@@ -46,9 +54,9 @@ export function getResourceWeight(itemId: number, stratum: number): number {
46
54
 
47
55
  switch (tier) {
48
56
  case 1:
49
- if (stratum < 2000) return 100
50
- if (stratum < 10000) return 80
51
- if (stratum < 30000) return 50
57
+ if (stratum < DEPTH_THRESHOLD_T2) return 100
58
+ if (stratum < DEPTH_THRESHOLD_T3) return 80
59
+ if (stratum < DEPTH_THRESHOLD_T4) return 50
52
60
  return 30
53
61
  case 2:
54
62
  if (depthAbove < 3000) return 40
@@ -1,6 +1,7 @@
1
1
  import {UInt64, type UInt64Type} from '@wharfkit/antelope'
2
2
  import {ServerContract} from '../contracts'
3
3
  import type {CoordinatesType} from '../types'
4
+ import {type FloatPosition, getInterpolatedPosition} from '../travel/travel'
4
5
  import {Location} from './location'
5
6
  import {ScheduleAccessor} from '../scheduling/accessor'
6
7
  import * as schedule from '../scheduling/schedule'
@@ -24,6 +25,14 @@ export class Container extends ServerContract.Types.entity_info {
24
25
  return this.entity_name
25
26
  }
26
27
 
28
+ get entityClass(): 'mobile' {
29
+ return 'mobile'
30
+ }
31
+
32
+ get canUndeploy(): boolean {
33
+ return true
34
+ }
35
+
27
36
  get sched(): ScheduleAccessor {
28
37
  this._sched ??= new ScheduleAccessor(this)
29
38
  return this._sched
@@ -33,6 +42,12 @@ export class Container extends ServerContract.Types.entity_info {
33
42
  return this.is_idle
34
43
  }
35
44
 
45
+ interpolatedPositionAt(now: Date): FloatPosition {
46
+ const taskIndex = this.sched.currentTaskIndex(now)
47
+ const progress = this.sched.currentTaskProgressFloat(now)
48
+ return getInterpolatedPosition(this, taskIndex, progress)
49
+ }
50
+
36
51
  isLoading(now: Date): boolean {
37
52
  return schedule.isLoading(this, now)
38
53
  }
@@ -75,10 +90,10 @@ export function computeContainerCapabilities(stats: Record<string, number>): {
75
90
  hullmass: number
76
91
  capacity: number
77
92
  } {
78
- const density = stats['density'] ?? 500
79
- const strength = stats['strength'] ?? 500
80
- const hardness = stats['hardness'] ?? 500
81
- const saturation = stats['saturation'] ?? 500
93
+ const density = stats.density
94
+ const strength = stats.strength
95
+ const hardness = stats.hardness
96
+ const saturation = stats.saturation
82
97
 
83
98
  const hullmass = 25000 + 75 * density
84
99
 
@@ -93,10 +108,10 @@ export function computeContainerT2Capabilities(stats: Record<string, number>): {
93
108
  hullmass: number
94
109
  capacity: number
95
110
  } {
96
- const strength = stats['strength'] ?? 0
97
- const density = stats['density'] ?? 0
98
- const hardness = stats['hardness'] ?? 0
99
- const saturation = stats['saturation'] ?? 0
111
+ const strength = stats.strength
112
+ const density = stats.density
113
+ const hardness = stats.hardness
114
+ const saturation = stats.saturation
100
115
 
101
116
  const hullmass = 20000 + 50 * density
102
117
 
@@ -8,15 +8,16 @@ import {
8
8
  MODULE_HAULER,
9
9
  MODULE_LOADER,
10
10
  } from '../capabilities/modules'
11
+ import {getItem} from '../data/catalog'
11
12
 
12
13
  export function computeShipHullCapabilities(stats: Record<string, number>): {
13
14
  hullmass: number
14
15
  capacity: number
15
16
  } {
16
- const density = stats.density ?? 500
17
- const strength = stats.strength ?? 500
18
- const hardness = stats.hardness ?? 500
19
- const saturation = stats.saturation ?? 500
17
+ const density = stats.density
18
+ const strength = stats.strength
19
+ const hardness = stats.hardness
20
+ const saturation = stats.saturation
20
21
 
21
22
  const hullmass = 25000 + 75 * density
22
23
  const statSum = strength + hardness + saturation
@@ -30,8 +31,8 @@ export function computeEngineCapabilities(stats: Record<string, number>): {
30
31
  thrust: number
31
32
  drain: number
32
33
  } {
33
- const vol = stats.volatility ?? 500
34
- const thm = stats.thermal ?? 500
34
+ const vol = stats.volatility
35
+ const thm = stats.thermal
35
36
 
36
37
  return {
37
38
  thrust: 400 + Math.floor((vol * 3) / 4),
@@ -43,30 +44,61 @@ export function computeGeneratorCapabilities(stats: Record<string, number>): {
43
44
  capacity: number
44
45
  recharge: number
45
46
  } {
46
- const res = stats.resonance ?? 500
47
- const ref = stats.reflectivity ?? 500
47
+ const com = stats.composition
48
+ const fin = stats.fineness
48
49
 
49
50
  return {
50
- capacity: 300 + Math.floor(res / 6),
51
- recharge: 1 + Math.floor((ref * 3) / 1000),
51
+ capacity: 300 + Math.floor(com / 6),
52
+ recharge: 1 + Math.floor((fin * 3) / 1000),
52
53
  }
53
54
  }
54
55
 
55
- export function computeGathererCapabilities(stats: Record<string, number>): {
56
+ export interface GathererDepthParams {
57
+ readonly floor: number
58
+ readonly slope: number
59
+ }
60
+
61
+ export const GATHERER_DEPTH_TABLE: readonly GathererDepthParams[] = [
62
+ {floor: 500, slope: 5},
63
+ {floor: 2000, slope: 11},
64
+ {floor: 7000, slope: 16},
65
+ {floor: 15000, slope: 18},
66
+ {floor: 25000, slope: 19},
67
+ {floor: 35000, slope: 16},
68
+ {floor: 46000, slope: 12},
69
+ {floor: 53500, slope: 10},
70
+ {floor: 60000, slope: 5},
71
+ {floor: 63500, slope: 2},
72
+ ]
73
+
74
+ export const GATHERER_DEPTH_MAX_TIER = 10
75
+
76
+ export function gathererDepthForTier(tol: number, tier: number): number {
77
+ if (tier < 1 || tier > GATHERER_DEPTH_MAX_TIER) {
78
+ throw new Error(`gatherer tier out of range: ${tier}`)
79
+ }
80
+ const p = GATHERER_DEPTH_TABLE[tier - 1]
81
+ return p.floor + tol * p.slope
82
+ }
83
+
84
+ export function computeGathererCapabilities(
85
+ stats: Record<string, number>,
86
+ tier: number
87
+ ): {
56
88
  yield: number
57
89
  drain: number
58
90
  depth: number
59
91
  speed: number
60
92
  } {
61
- const str = stats.strength ?? 500
62
- const con = stats.conductivity ?? 500
63
- const ref = stats.reflectivity ?? 500
64
- const tol = stats.tolerance ?? 500
93
+ const str = stats.strength
94
+ const con = stats.conductivity
95
+ const ref = stats.reflectivity
96
+ const tol = stats.tolerance
65
97
 
66
98
  return {
67
99
  yield: 200 + str,
68
100
  drain: Math.max(250, 1250 - Math.floor((con * 25) / 20)),
69
- depth: 200 + Math.floor((tol * 3) / 2),
101
+ depth: gathererDepthForTier(tol, tier),
70
102
  speed: 100 + Math.floor((ref * 4) / 5),
71
103
  }
72
104
  }
@@ -76,12 +108,12 @@ export function computeLoaderCapabilities(stats: Record<string, number>): {
76
108
  thrust: number
77
109
  quantity: number
78
110
  } {
79
- const hrd = stats.hardness ?? 500
80
- const pla = stats.plasticity ?? 500
111
+ const insulation = stats.insulation
112
+ const plasticity = stats.plasticity
81
113
 
82
114
  return {
83
- mass: Math.max(200, 2000 - Math.floor(hrd * 2)),
84
- thrust: 1 + Math.floor(pla / 500),
115
+ mass: Math.max(200, 2000 - Math.floor(insulation * 2)),
116
+ thrust: 1 + Math.floor(plasticity / 500),
85
117
  quantity: 1,
86
118
  }
87
119
  }
@@ -90,12 +122,12 @@ export function computeCrafterCapabilities(stats: Record<string, number>): {
90
122
  speed: number
91
123
  drain: number
92
124
  } {
93
- const rea = stats.reactivity ?? 500
94
- const com = stats.composition ?? 500
125
+ const rea = stats.reactivity
126
+ const fin = stats.fineness
95
127
 
96
128
  return {
97
129
  speed: 100 + Math.floor((rea * 4) / 5),
98
- drain: Math.max(5, 30 - Math.floor(com / 33)),
130
+ drain: Math.max(5, 30 - Math.floor(fin / 33)),
99
131
  }
100
132
  }
101
133
 
@@ -104,14 +136,14 @@ export function computeHaulerCapabilities(stats: Record<string, number>): {
104
136
  efficiency: number
105
137
  drain: number
106
138
  } {
107
- const res = stats.resonance ?? 500
108
- const con = stats.conductivity ?? 500
109
- const ref = stats.reflectivity ?? 500
139
+ const fineness = stats.fineness
140
+ const conductivity = stats.conductivity
141
+ const composition = stats.composition
110
142
 
111
143
  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)),
144
+ capacity: Math.max(1, 1 + Math.floor(fineness / 400)),
145
+ efficiency: 2000 + conductivity * 6,
146
+ drain: Math.max(3, 15 - Math.floor(composition / 80)),
115
147
  }
116
148
  }
117
149
 
@@ -121,11 +153,12 @@ export function computeStorageCapabilities(
121
153
  ): {
122
154
  capacityBonus: number
123
155
  } {
124
- const strength = stats.strength ?? 500
125
- const hardness = stats.hardness ?? 500
126
- const saturation = stats.saturation ?? 500
156
+ const strength = stats.strength
157
+ const density = stats.density
158
+ const hardness = stats.hardness
159
+ const saturation = stats.saturation
127
160
 
128
- const statSum = strength + hardness + saturation
161
+ const statSum = strength + density + hardness + saturation
129
162
  const capacityBonus = Math.floor(
130
163
  (baseCapacity * (10 + Math.floor((statSum * 10) / 2997))) / 100
131
164
  )
@@ -137,10 +170,10 @@ export function computeWarehouseHullCapabilities(stats: Record<string, number>):
137
170
  hullmass: number
138
171
  capacity: number
139
172
  } {
140
- const density = stats.density ?? 500
141
- const strength = stats.strength ?? 500
142
- const hardness = stats.hardness ?? 500
143
- const saturation = stats.saturation ?? 500
173
+ const density = stats.density
174
+ const strength = stats.strength
175
+ const hardness = stats.hardness
176
+ const saturation = stats.saturation
144
177
 
145
178
  const hullmass = 25000 + 75 * density
146
179
  const statSum = strength + hardness + saturation
@@ -196,16 +229,20 @@ export function computeShipCapabilities(
196
229
  if (gathererModules.length > 0) {
197
230
  let totalYield = 0
198
231
  let totalDrain = 0
199
- let totalDepth = 0
232
+ let maxDepth = 0
200
233
  let totalSpeed = 0
201
234
  for (const m of gathererModules) {
202
- const caps = computeGathererCapabilities(decodeCraftedItemStats(m.itemId, m.stats))
235
+ const tier = getItem(m.itemId).tier
236
+ const caps = computeGathererCapabilities(
237
+ decodeCraftedItemStats(m.itemId, m.stats),
238
+ tier
239
+ )
203
240
  totalYield += caps.yield
204
241
  totalDrain += caps.drain
205
- totalDepth += caps.depth
242
+ if (caps.depth > maxDepth) maxDepth = caps.depth
206
243
  totalSpeed += caps.speed
207
244
  }
208
- ship.gatherer = {yield: totalYield, drain: totalDrain, depth: totalDepth, speed: totalSpeed}
245
+ ship.gatherer = {yield: totalYield, drain: totalDrain, depth: maxDepth, speed: totalSpeed}
209
246
  }
210
247
 
211
248
  const haulerModules = modules.filter((m) => getModuleCapabilityType(m.itemId) === MODULE_HAULER)
@@ -2,7 +2,9 @@ import {type UInt16, type UInt16Type, UInt32, UInt64, type UInt64Type} from '@wh
2
2
  import {ServerContract} from '../contracts'
3
3
  import {Coordinates, type CoordinatesType} from '../types'
4
4
  import {
5
+ type FloatPosition,
5
6
  getDestinationLocation,
7
+ getInterpolatedPosition,
6
8
  getPositionAt,
7
9
  getFlightOrigin as travelGetFlightOrigin,
8
10
  } from '../travel/travel'
@@ -55,6 +57,14 @@ export class Ship extends ServerContract.Types.entity_info {
55
57
  return this.entity_name
56
58
  }
57
59
 
60
+ get entityClass(): 'mobile' {
61
+ return 'mobile'
62
+ }
63
+
64
+ get canUndeploy(): boolean {
65
+ return true
66
+ }
67
+
58
68
  get inv(): InventoryAccessor {
59
69
  this._inv ??= new InventoryAccessor(this)
60
70
  return this._inv
@@ -87,12 +97,19 @@ export class Ship extends ServerContract.Types.entity_info {
87
97
  return dest ? Coordinates.from(dest) : undefined
88
98
  }
89
99
 
100
+ /** Chain-tile coordinates at `now`. For smooth visual position use interpolatedPositionAt. */
90
101
  positionAt(now: Date): Coordinates {
91
102
  const taskIndex = this.sched.currentTaskIndex(now)
92
103
  const progress = this.sched.currentTaskProgress(now)
93
104
  return Coordinates.from(getPositionAt(this, taskIndex, progress))
94
105
  }
95
106
 
107
+ interpolatedPositionAt(now: Date): FloatPosition {
108
+ const taskIndex = this.sched.currentTaskIndex(now)
109
+ const progress = this.sched.currentTaskProgressFloat(now)
110
+ return getInterpolatedPosition(this, taskIndex, progress)
111
+ }
112
+
96
113
  isInFlight(now: Date): boolean {
97
114
  return schedule.isInFlight(this, now)
98
115
  }
@@ -31,6 +31,14 @@ export class Warehouse extends ServerContract.Types.entity_info {
31
31
  return this.entity_name
32
32
  }
33
33
 
34
+ get entityClass(): 'building' {
35
+ return 'building'
36
+ }
37
+
38
+ get canDemolish(): boolean {
39
+ return true
40
+ }
41
+
34
42
  get inv(): InventoryAccessor {
35
43
  this._inv ??= new InventoryAccessor(this)
36
44
  return this._inv