@shipload/sdk 1.0.0-next.7 → 1.0.0-next.70

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 (203) hide show
  1. package/lib/scan.d.ts +52 -0
  2. package/lib/scan.js +162 -0
  3. package/lib/scan.js.map +1 -0
  4. package/lib/scan.m.js +152 -0
  5. package/lib/scan.m.js.map +1 -0
  6. package/lib/shipload.d.ts +5696 -1503
  7. package/lib/shipload.js +20294 -5200
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +20371 -5683
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1648 -0
  12. package/lib/testing.js +6980 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +6974 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.test.ts +82 -0
  18. package/src/capabilities/craftable.ts +82 -0
  19. package/src/capabilities/crafting.test.ts +80 -0
  20. package/src/capabilities/crafting.ts +50 -8
  21. package/src/capabilities/gathering.test.ts +29 -0
  22. package/src/capabilities/gathering.ts +38 -20
  23. package/src/capabilities/hauling.ts +0 -5
  24. package/src/capabilities/index.ts +0 -1
  25. package/src/capabilities/modules.ts +27 -30
  26. package/src/capabilities/movement.ts +1 -1
  27. package/src/capabilities/storage.test.ts +26 -0
  28. package/src/capabilities/storage.ts +22 -7
  29. package/src/contracts/fund.ts +306 -0
  30. package/src/contracts/platform.ts +312 -3
  31. package/src/contracts/server.ts +2661 -466
  32. package/src/contracts/token.ts +170 -0
  33. package/src/contracts.ts +2 -0
  34. package/src/coordinates/address.ts +88 -0
  35. package/src/coordinates/constants.test.ts +15 -0
  36. package/src/coordinates/constants.ts +23 -0
  37. package/src/coordinates/index.ts +15 -0
  38. package/src/coordinates/memo.test.ts +47 -0
  39. package/src/coordinates/memo.ts +20 -0
  40. package/src/coordinates/permutation.ts +77 -0
  41. package/src/coordinates/regions.ts +48 -0
  42. package/src/coordinates/sectors.ts +115 -0
  43. package/src/data/capabilities.ts +49 -18
  44. package/src/data/capability-formulas.ts +26 -14
  45. package/src/data/catalog.test.ts +19 -0
  46. package/src/data/catalog.ts +13 -3
  47. package/src/data/charters.json +396 -0
  48. package/src/data/citizenry-adjectives.json +42 -0
  49. package/src/data/citizenry-nouns.json +50 -0
  50. package/src/data/colors.ts +14 -32
  51. package/src/data/entities.json +421 -15
  52. package/src/data/item-ids.ts +53 -13
  53. package/src/data/items.json +330 -77
  54. package/src/data/kind-registry.json +212 -0
  55. package/src/data/kind-registry.ts +192 -0
  56. package/src/data/metadata.ts +312 -51
  57. package/src/data/recipes-runtime.ts +6 -23
  58. package/src/data/recipes.json +2173 -173
  59. package/src/data/tiers.ts +9 -6
  60. package/src/derivation/build-methods.test.ts +71 -0
  61. package/src/derivation/build-methods.ts +50 -0
  62. package/src/derivation/capabilities.test.ts +348 -0
  63. package/src/derivation/capabilities.ts +707 -0
  64. package/src/derivation/capability-mappings.ts +60 -16
  65. package/src/derivation/crafting.test.ts +17 -0
  66. package/src/derivation/crafting.ts +57 -34
  67. package/src/derivation/index.ts +27 -2
  68. package/src/derivation/recipe-usage.test.ts +88 -0
  69. package/src/derivation/recipe-usage.ts +141 -0
  70. package/src/derivation/reserve-regen.ts +34 -0
  71. package/src/derivation/resources.ts +9 -1
  72. package/src/derivation/rollups.test.ts +55 -0
  73. package/src/derivation/rollups.ts +72 -0
  74. package/src/derivation/stars.test.ts +67 -0
  75. package/src/derivation/stars.ts +25 -0
  76. package/src/derivation/stat-scaling.ts +12 -0
  77. package/src/derivation/stats.ts +6 -6
  78. package/src/derivation/strata.ts +5 -3
  79. package/src/derivation/stratum.ts +26 -22
  80. package/src/derivation/tiers.ts +40 -7
  81. package/src/derivation/upgrades.ts +14 -0
  82. package/src/derivation/wormhole.ts +154 -0
  83. package/src/entities/entity.ts +102 -0
  84. package/src/entities/gamestate.ts +3 -28
  85. package/src/entities/makers.ts +144 -136
  86. package/src/entities/slot-multiplier.ts +43 -0
  87. package/src/entity/hydrate.test.ts +156 -0
  88. package/src/entity/hydrate.ts +86 -0
  89. package/src/errors.ts +21 -16
  90. package/src/format.ts +42 -9
  91. package/src/index-module.ts +411 -32
  92. package/src/influence/ballot.test.ts +103 -0
  93. package/src/influence/ballot.ts +98 -0
  94. package/src/influence/categories.ts +32 -0
  95. package/src/influence/charter-metadata.test.ts +136 -0
  96. package/src/influence/charter-metadata.ts +236 -0
  97. package/src/influence/charters.test.ts +112 -0
  98. package/src/influence/charters.ts +150 -0
  99. package/src/influence/citizenry.ts +38 -0
  100. package/src/influence/civic-shuttle.test.ts +202 -0
  101. package/src/influence/civic-shuttle.ts +142 -0
  102. package/src/influence/civic.test.ts +287 -0
  103. package/src/influence/civic.ts +269 -0
  104. package/src/influence/constants.ts +84 -0
  105. package/src/influence/decay.ts +20 -0
  106. package/src/influence/decomp.ts +95 -0
  107. package/src/influence/demand.ts +106 -0
  108. package/src/influence/duration.ts +101 -0
  109. package/src/influence/index.ts +118 -0
  110. package/src/influence/influence.test.ts +450 -0
  111. package/src/influence/quality.ts +25 -0
  112. package/src/influence/valuation.ts +154 -0
  113. package/src/managers/actions.ts +940 -93
  114. package/src/managers/balances.test.ts +161 -0
  115. package/src/managers/balances.ts +93 -0
  116. package/src/managers/base.ts +10 -2
  117. package/src/managers/cluster.test.ts +39 -0
  118. package/src/managers/cluster.ts +53 -0
  119. package/src/managers/construction-types.ts +80 -0
  120. package/src/managers/construction.ts +453 -0
  121. package/src/managers/context.ts +58 -1
  122. package/src/managers/coordinates.ts +14 -0
  123. package/src/managers/entities.ts +35 -63
  124. package/src/managers/epochs.ts +72 -1
  125. package/src/managers/index.ts +38 -1
  126. package/src/managers/influence.test.ts +56 -0
  127. package/src/managers/influence.ts +425 -0
  128. package/src/managers/jobs.test.ts +80 -0
  129. package/src/managers/jobs.ts +54 -0
  130. package/src/managers/locations.ts +25 -29
  131. package/src/managers/nft.test.ts +21 -0
  132. package/src/managers/nft.ts +110 -0
  133. package/src/managers/players.ts +25 -0
  134. package/src/managers/plot.ts +122 -0
  135. package/src/nft/atomicassets.abi.json +1342 -0
  136. package/src/nft/atomicassets.ts +237 -0
  137. package/src/nft/atomicdata.ts +130 -0
  138. package/src/nft/buildImmutableData.ts +388 -0
  139. package/src/nft/description.ts +222 -100
  140. package/src/nft/index.ts +3 -0
  141. package/src/planner/index.ts +296 -0
  142. package/src/planner/planner.test.ts +459 -0
  143. package/src/receipts.ts +22 -0
  144. package/src/resolution/describe-module.ts +43 -25
  145. package/src/resolution/display-name.ts +39 -11
  146. package/src/resolution/resolve-item.test.ts +48 -0
  147. package/src/resolution/resolve-item.ts +112 -58
  148. package/src/scan/index.ts +244 -0
  149. package/src/scan/scan-wasm.base64.ts +2 -0
  150. package/src/scheduling/accessor.ts +65 -23
  151. package/src/scheduling/availability.test.ts +320 -0
  152. package/src/scheduling/availability.ts +251 -0
  153. package/src/scheduling/cancel.test.ts +550 -0
  154. package/src/scheduling/cancel.ts +254 -0
  155. package/src/scheduling/cluster-stock.test.ts +31 -0
  156. package/src/scheduling/cluster-stock.ts +15 -0
  157. package/src/scheduling/energy.ts +47 -0
  158. package/src/scheduling/epoch.ts +1 -0
  159. package/src/scheduling/idle-resolve.ts +45 -0
  160. package/src/scheduling/jobs.test.ts +49 -0
  161. package/src/scheduling/jobs.ts +137 -0
  162. package/src/scheduling/lane-core.ts +128 -0
  163. package/src/scheduling/lanes.test.ts +250 -0
  164. package/src/scheduling/lanes.ts +227 -0
  165. package/src/scheduling/projection.ts +265 -149
  166. package/src/scheduling/schedule.test.ts +163 -0
  167. package/src/scheduling/schedule.ts +282 -116
  168. package/src/scheduling/task-cargo.test.ts +110 -0
  169. package/src/scheduling/task-cargo.ts +62 -0
  170. package/src/scheduling/task-effects.test.ts +35 -0
  171. package/src/scheduling/task-effects.ts +34 -0
  172. package/src/scheduling/unwrap.test.ts +86 -0
  173. package/src/scheduling/unwrap.ts +190 -0
  174. package/src/shipload.ts +63 -4
  175. package/src/subscriptions/manager.cluster.test.ts +51 -0
  176. package/src/subscriptions/manager.ts +245 -172
  177. package/src/subscriptions/mappers.ts +5 -8
  178. package/src/subscriptions/types.ts +28 -3
  179. package/src/testing/catalog-hash.ts +19 -0
  180. package/src/testing/index.ts +2 -0
  181. package/src/testing/projection-parity.ts +167 -0
  182. package/src/travel/reach.test.ts +86 -0
  183. package/src/travel/reach.ts +34 -0
  184. package/src/travel/route-build.test.ts +63 -0
  185. package/src/travel/route-build.ts +60 -0
  186. package/src/travel/route-planner.ts +288 -0
  187. package/src/travel/route-simulator.ts +170 -0
  188. package/src/travel/travel.ts +190 -123
  189. package/src/types/capabilities.ts +37 -8
  190. package/src/types/entity.ts +3 -3
  191. package/src/types/index.ts +0 -1
  192. package/src/types.ts +81 -23
  193. package/src/utils/cargo.test.ts +14 -0
  194. package/src/utils/cargo.ts +2 -0
  195. package/src/utils/display-name.ts +70 -0
  196. package/src/utils/hash.ts +22 -2
  197. package/src/utils/system.ts +24 -5
  198. package/src/capabilities/loading.ts +0 -8
  199. package/src/entities/container.ts +0 -123
  200. package/src/entities/ship-deploy.ts +0 -295
  201. package/src/entities/ship.ts +0 -221
  202. package/src/entities/warehouse.ts +0 -127
  203. package/src/types/entity-traits.ts +0 -69
@@ -0,0 +1,82 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {ServerContract} from '../contracts'
3
+ import {UInt16, UInt32, UInt64} from '@wharfkit/antelope'
4
+ import {
5
+ ITEM_ENGINE_T1,
6
+ ITEM_GENERATOR_T1,
7
+ ITEM_CRAFTER_T1,
8
+ ITEM_PLASMA_CELL,
9
+ ITEM_SHIP_T1_PACKED,
10
+ } from '../data/item-ids'
11
+ import {getRecipe} from '../data/recipes-runtime'
12
+ import {encodeStats} from '../derivation/crafting'
13
+ import {rollupCrafter} from '../derivation/rollups'
14
+ import {makeEntity} from '../entities/makers'
15
+ import type {IncomingSource} from '../scheduling/availability'
16
+ import {maxCraftable} from './craftable'
17
+
18
+ const NOW = new Date('2026-06-11T17:06:40.000Z')
19
+
20
+ function ship(cargo: ServerContract.Types.cargo_item[] = []) {
21
+ return makeEntity(ITEM_SHIP_T1_PACKED, {
22
+ id: 99,
23
+ owner: 'tester.gm',
24
+ name: 'Craft Tester',
25
+ coordinates: {x: 0, y: 0},
26
+ hullmass: 200_000,
27
+ capacity: 8_000_000,
28
+ cargomass: 0,
29
+ energy: 500_000,
30
+ modules: [
31
+ {itemId: ITEM_GENERATOR_T1, stats: encodeStats([999, 999])},
32
+ {itemId: ITEM_CRAFTER_T1, stats: encodeStats([999, 999])},
33
+ ],
34
+ cargo,
35
+ })
36
+ }
37
+
38
+ function crafterSpeed(entity: ReturnType<typeof ship>): number {
39
+ return rollupCrafter(entity.crafter_lanes)!.speed.toNumber()
40
+ }
41
+
42
+ function plasmaCell(quantity: number) {
43
+ return ServerContract.Types.cargo_item.from({
44
+ item_id: UInt16.from(ITEM_PLASMA_CELL),
45
+ quantity: UInt32.from(quantity),
46
+ stats: UInt64.from(0),
47
+ modules: [],
48
+ })
49
+ }
50
+
51
+ describe('maxCraftable — incoming', () => {
52
+ test('on-hand-only baseline stays 0 for an empty-cargo entity', () => {
53
+ const recipe = getRecipe(ITEM_ENGINE_T1)!
54
+ const entity = ship([])
55
+ expect(maxCraftable(entity, recipe, crafterSpeed(entity), NOW)).toBe(0)
56
+ })
57
+
58
+ test('incoming source alone covers the recipe inputs', () => {
59
+ const recipe = getRecipe(ITEM_ENGINE_T1)!
60
+ const entity = ship([])
61
+ const incoming: IncomingSource[] = [
62
+ {holdId: '1', until: new Date(NOW.getTime() - 1000), items: [plasmaCell(1800)]},
63
+ ]
64
+
65
+ expect(maxCraftable(entity, recipe, crafterSpeed(entity), NOW)).toBe(0)
66
+ expect(maxCraftable(entity, recipe, crafterSpeed(entity), NOW, incoming)).toBe(3)
67
+ })
68
+
69
+ test('an incoming source arriving well after craft completion still resolves (eventually craftable)', () => {
70
+ const recipe = getRecipe(ITEM_ENGINE_T1)!
71
+ const entity = ship([])
72
+ const farFuture: IncomingSource[] = [
73
+ {
74
+ holdId: '1',
75
+ until: new Date(NOW.getTime() + 1_000_000_000),
76
+ items: [plasmaCell(1800)],
77
+ },
78
+ ]
79
+
80
+ expect(maxCraftable(entity, recipe, crafterSpeed(entity), NOW, farFuture)).toBe(3)
81
+ })
82
+ })
@@ -0,0 +1,82 @@
1
+ import type {ServerContract} from '../contracts'
2
+ import {getItem} from '../data/catalog'
3
+ import type {Recipe} from '../data/recipes-runtime'
4
+ import {
5
+ availableForItem,
6
+ projectedCargoAvailableAt,
7
+ taskCargoEffect,
8
+ type IncomingSource,
9
+ } from '../scheduling/availability'
10
+ import {candidateLaneCompletesAt, workerLaneKey} from '../scheduling/lanes'
11
+ import * as schedule from '../scheduling/schedule'
12
+ import type {ScheduleData} from '../scheduling/schedule'
13
+ import {calc_craft_duration} from './crafting'
14
+
15
+ type CargoItem = ServerContract.Types.cargo_item
16
+ type ModuleEntry = ServerContract.Types.module_entry
17
+
18
+ export interface CraftableEntity extends ScheduleData {
19
+ cargo: CargoItem[]
20
+ modules: ModuleEntry[]
21
+ }
22
+
23
+ // Recipe inputs aren't stat-pinned (any variant counts, mirroring availableForItem's item-id aggregation).
24
+ function itemReadyAt(
25
+ entity: CraftableEntity,
26
+ itemIds: readonly number[],
27
+ incoming: readonly IncomingSource[]
28
+ ): Date {
29
+ let readyMs = 0
30
+ for (const ordered of schedule.orderedTasks(entity)) {
31
+ for (const item of taskCargoEffect(ordered.task).added) {
32
+ if (itemIds.includes(item.item_id.toNumber())) {
33
+ readyMs = Math.max(readyMs, ordered.completesAt.getTime())
34
+ break
35
+ }
36
+ }
37
+ }
38
+ for (const src of incoming) {
39
+ if (src.items.some((item) => itemIds.includes(item.item_id.toNumber()))) {
40
+ readyMs = Math.max(readyMs, src.until.getTime())
41
+ }
42
+ }
43
+ return new Date(readyMs)
44
+ }
45
+
46
+ export function maxCraftable(
47
+ entity: CraftableEntity,
48
+ recipe: Recipe,
49
+ crafterSpeed: number,
50
+ now: Date,
51
+ incoming: readonly IncomingSource[] = []
52
+ ): number {
53
+ if (recipe.inputs.length === 0) return 0
54
+
55
+ const perUnitMass = recipe.inputs.reduce(
56
+ (sum, input) => sum + getItem(input.itemId).mass * input.quantity,
57
+ 0
58
+ )
59
+ const perUnitDuration = calc_craft_duration(crafterSpeed, perUnitMass).toNumber()
60
+ const crafterLane = workerLaneKey(entity.modules, 'crafter', entity.lanes ?? [])
61
+ const naiveCompletesAt = candidateLaneCompletesAt(entity, crafterLane, perUnitDuration, now)
62
+ const laneStartMs = naiveCompletesAt.getTime() - perUnitDuration * 1000
63
+ const readyMs = itemReadyAt(
64
+ entity,
65
+ recipe.inputs.map((input) => input.itemId),
66
+ incoming
67
+ ).getTime()
68
+ const completesAt = new Date(Math.max(laneStartMs, readyMs) + perUnitDuration * 1000)
69
+ const availability = projectedCargoAvailableAt(entity, completesAt, incoming)
70
+
71
+ let maxUnits: bigint | undefined
72
+ for (const input of recipe.inputs) {
73
+ if (input.quantity <= 0) return 0
74
+
75
+ const units = availableForItem(availability, input.itemId) / BigInt(input.quantity)
76
+ maxUnits = maxUnits === undefined || units < maxUnits ? units : maxUnits
77
+ }
78
+
79
+ if (maxUnits === undefined) return 0
80
+ if (maxUnits > BigInt(Number.MAX_SAFE_INTEGER)) return Number.MAX_SAFE_INTEGER
81
+ return Number(maxUnits)
82
+ }
@@ -0,0 +1,80 @@
1
+ import {UInt16, UInt32} from '@wharfkit/antelope'
2
+ import {describe, expect, test} from 'bun:test'
3
+ import type {ServerContract} from '../contracts'
4
+ import {computeInputMass} from '../derivation/crafting'
5
+ import {getRecipe} from '../data/recipes-runtime'
6
+ import {
7
+ calc_craft_energy,
8
+ calcClusterIntake,
9
+ calcClustercraftDuration,
10
+ craftEnergyCost,
11
+ INTAKE_RATE,
12
+ } from './crafting'
13
+
14
+ function crafterLane(drain: number): ServerContract.Types.crafter_lane {
15
+ return {
16
+ slot_index: UInt16.from(0),
17
+ speed: UInt16.from(1000),
18
+ drain: UInt32.from(drain),
19
+ output_pct: UInt16.from(100),
20
+ } as unknown as ServerContract.Types.crafter_lane
21
+ }
22
+
23
+ test('calc_craft_energy does not clamp above the old uint16 ceiling', () => {
24
+ const energy = calc_craft_energy(30, 400_000_000)
25
+ expect(Number(energy)).toBeGreaterThan(65535)
26
+ })
27
+
28
+ describe('calcClusterIntake', () => {
29
+ test('is floor(sourcedMass / INTAKE_RATE)', () => {
30
+ expect(INTAKE_RATE).toBe(360)
31
+ expect(calcClusterIntake(360)).toBe(1)
32
+ expect(calcClusterIntake(359)).toBe(0)
33
+ expect(calcClusterIntake(196_800)).toBe(546)
34
+ })
35
+
36
+ test('zero sourced mass (all own-hold) charges no intake', () => {
37
+ expect(calcClusterIntake(0)).toBe(0)
38
+ })
39
+ })
40
+
41
+ describe('calcClustercraftDuration', () => {
42
+ test('adds intake to the base craft duration', () => {
43
+ // calc_craft_duration = floor(inputMass/speed) + 1
44
+ const speed = 1000
45
+ const inputMass = 1000 // base craft = 100 + 1 = 101s
46
+ const sourcedMass = 720 // intake = 2s
47
+ expect(calcClustercraftDuration(speed, inputMass, sourcedMass).toNumber()).toBe(103)
48
+ })
49
+ })
50
+
51
+ describe('craftEnergyCost', () => {
52
+ const OUTPUT_ITEM_ID = 10001
53
+ const DRAIN = 100_000
54
+
55
+ test('matches calc_craft_energy against the recipe input mass times units', () => {
56
+ const recipe = getRecipe(OUTPUT_ITEM_ID)!
57
+ const lane = crafterLane(DRAIN)
58
+ const units = 5
59
+
60
+ const cost = craftEnergyCost(lane, recipe, units)
61
+
62
+ const expected = Number(calc_craft_energy(DRAIN, computeInputMass(OUTPUT_ITEM_ID) * units))
63
+ expect(cost).toBe(expected)
64
+ })
65
+
66
+ test('is 0 for non-positive units', () => {
67
+ const recipe = getRecipe(OUTPUT_ITEM_ID)!
68
+ const lane = crafterLane(DRAIN)
69
+ expect(craftEnergyCost(lane, recipe, 0)).toBe(0)
70
+ expect(craftEnergyCost(lane, recipe, -1)).toBe(0)
71
+ })
72
+
73
+ test('scales with unit count', () => {
74
+ const recipe = getRecipe(OUTPUT_ITEM_ID)!
75
+ const lane = crafterLane(DRAIN)
76
+ const one = craftEnergyCost(lane, recipe, 1)
77
+ const ten = craftEnergyCost(lane, recipe, 10)
78
+ expect(ten).toBeGreaterThan(one)
79
+ })
80
+ })
@@ -1,10 +1,12 @@
1
- import {UInt16, UInt32} from '@wharfkit/antelope'
2
- import {CRAFT_ENERGY_DIVISOR} from '../types'
3
- import type {EntityCapabilities} from '../types/capabilities'
1
+ import {UInt32} from '@wharfkit/antelope'
2
+ import {CRAFT_ENERGY_DIVISOR, MASS_STAT_SCALE} from '../types'
3
+ import type {CrafterStats, EntityCapabilities} from '../types/capabilities'
4
4
  import type {ServerContract} from '../contracts'
5
+ import {getItem} from '../data/catalog'
6
+ import type {Recipe} from '../data/recipes-runtime'
5
7
 
6
8
  export interface CrafterCapability {
7
- crafter: ServerContract.Types.crafter_stats
9
+ crafter: CrafterStats
8
10
  }
9
11
 
10
12
  export function capsHasCrafter(caps: EntityCapabilities): boolean {
@@ -12,11 +14,51 @@ export function capsHasCrafter(caps: EntityCapabilities): boolean {
12
14
  }
13
15
 
14
16
  export function calc_craft_duration(speed: number, totalInputMass: number): UInt32 {
15
- const duration = Math.floor(totalInputMass / speed)
16
- return UInt32.from(Math.max(duration, 1))
17
+ const duration = Math.floor((totalInputMass * MASS_STAT_SCALE) / speed)
18
+ return UInt32.from(duration + 1)
17
19
  }
18
20
 
19
- export function calc_craft_energy(drain: number, totalInputMass: number): UInt16 {
21
+ export const INTAKE_RATE = 360
22
+
23
+ // Mirrors calc_cluster_intake: whole seconds of intake for cargo sourced off other cluster members.
24
+ export function calcClusterIntake(sourcedMass: number): number {
25
+ return Math.floor(sourcedMass / INTAKE_RATE)
26
+ }
27
+
28
+ // Mirrors clustercraft duration: base craft duration + intake time.
29
+ export function calcClustercraftDuration(
30
+ speed: number,
31
+ totalInputMass: number,
32
+ sourcedMass: number
33
+ ): UInt32 {
34
+ return UInt32.from(
35
+ calc_craft_duration(speed, totalInputMass).toNumber() + calcClusterIntake(sourcedMass)
36
+ )
37
+ }
38
+
39
+ export function calc_craft_energy(drain: number, totalInputMass: number): UInt32 {
20
40
  const raw = Math.floor((totalInputMass * drain) / CRAFT_ENERGY_DIVISOR)
21
- return UInt16.from(Math.min(raw, 65535))
41
+ return UInt32.from(Math.min(Math.max(raw + 1, 1000), 4294967295))
42
+ }
43
+
44
+ export function craftEnergyCost(
45
+ lane: ServerContract.Types.crafter_lane,
46
+ recipe: Recipe,
47
+ units: number
48
+ ): number {
49
+ if (units <= 0) return 0
50
+ const inputMassPerUnit = recipe.inputs.reduce(
51
+ (sum, input) => sum + getItem(input.itemId).mass * input.quantity,
52
+ 0
53
+ )
54
+ return Number(calc_craft_energy(lane.drain.toNumber(), inputMassPerUnit * units))
55
+ }
56
+
57
+ // Craft-identical today; forked so build balance can diverge from craft without touching it.
58
+ export function calc_build_duration(speed: number, totalInputMass: number): UInt32 {
59
+ return calc_craft_duration(speed, totalInputMass)
60
+ }
61
+
62
+ export function calc_build_energy(drain: number, totalInputMass: number): UInt32 {
63
+ return calc_craft_energy(drain, totalInputMass)
22
64
  }
@@ -0,0 +1,29 @@
1
+ import {expect, test} from 'bun:test'
2
+ import type {GathererStats} from '../types/capabilities'
3
+ import {calc_gather_duration, calc_gather_energy} from './gathering'
4
+
5
+ function gatherer(drain: number): GathererStats {
6
+ return {
7
+ yield: {toNumber: () => 1},
8
+ drain: {toNumber: () => drain},
9
+ depth: {toNumber: () => 0, toString: () => '0'},
10
+ }
11
+ }
12
+
13
+ test('calc_gather_energy does not clamp above the old uint16 ceiling', () => {
14
+ const energy = calc_gather_energy(gatherer(30), 400_000_000)
15
+ expect(Number(energy)).toBeGreaterThan(65535)
16
+ })
17
+
18
+ test('splitting a bulk gather never undercuts total lane-time', () => {
19
+ const g: GathererStats = {
20
+ yield: {toNumber: () => 413},
21
+ drain: {toNumber: () => 30},
22
+ depth: {toNumber: () => 0, toString: () => '0'},
23
+ }
24
+ const bulk = Number(calc_gather_duration(g, 1000, 10, 8, 500))
25
+ const unit = Number(calc_gather_duration(g, 1000, 1, 8, 500))
26
+ // d1 >= 1 regime (bulk > quantity), where the split exploit lived
27
+ expect(bulk).toBeGreaterThan(10)
28
+ expect(unit * 10).toBeGreaterThanOrEqual(bulk)
29
+ })
@@ -1,36 +1,54 @@
1
- import {UInt16, UInt32} from '@wharfkit/antelope'
2
- import type {ServerContract} from '../contracts'
3
- import {PRECISION} from '../types'
1
+ import {UInt32} from '@wharfkit/antelope'
2
+ import type {GathererStats} from '../types/capabilities'
3
+ import {MASS_STAT_SCALE, PRECISION} from '../types'
4
4
 
5
5
  const GATHER_TIME_SCALE = 100
6
+ export const GATHER_MASS_DIVISOR = 228
6
7
  const DEPTH_PENALTY_DIVISOR = 5000
7
- const SPEED_TIME_SCALE = 300
8
8
 
9
- export function calc_gather_duration(
10
- gatherer: ServerContract.Types.gatherer_stats,
9
+ function gather_duration_raw(
10
+ gatherer: GathererStats,
11
11
  itemMass: number,
12
12
  quantity: number,
13
13
  stratum: number,
14
14
  richness: number
15
- ): UInt32 {
15
+ ): number {
16
16
  const yieldValue = gatherer.yield.toNumber()
17
- const speed = gatherer.speed.toNumber()
18
17
 
19
- if (yieldValue === 0 || speed === 0 || richness === 0) return UInt32.from(0)
18
+ if (yieldValue === 0 || richness === 0) return 0
20
19
 
21
- const massFactor = Math.sqrt(itemMass)
20
+ const massFactor = (itemMass * MASS_STAT_SCALE) / GATHER_MASS_DIVISOR
22
21
  const depthPenalty = 1 + stratum / DEPTH_PENALTY_DIVISOR
23
22
  const richnessMul = richness / 1000
24
- const gatherTime =
25
- (quantity * massFactor * GATHER_TIME_SCALE * depthPenalty) / (yieldValue * richnessMul)
26
- const speedTime = SPEED_TIME_SCALE * Math.log(1 + stratum / speed)
27
- return UInt32.from(Math.floor(gatherTime + speedTime))
23
+ return (quantity * massFactor * GATHER_TIME_SCALE * depthPenalty) / (yieldValue * richnessMul)
24
+ }
25
+
26
+ export function calc_gather_duration(
27
+ gatherer: GathererStats,
28
+ itemMass: number,
29
+ quantity: number,
30
+ stratum: number,
31
+ richness: number
32
+ ): UInt32 {
33
+ const raw = gather_duration_raw(gatherer, itemMass, quantity, stratum, richness)
34
+ // +1 per-gather setup cost mirrors the contract: splitting a bulk gather must not undercut lane-time
35
+ return UInt32.from(raw <= 0 ? 0 : Math.floor(raw) + 1)
36
+ }
37
+
38
+ export function calc_gather_rate(
39
+ gatherer: GathererStats,
40
+ itemMass: number,
41
+ stratum: number,
42
+ richness: number
43
+ ): {unitsPerSec: number; unitsPerMin: number; secPerUnit: number} {
44
+ const secPerUnit = gather_duration_raw(gatherer, itemMass, 1, stratum, richness)
45
+ if (secPerUnit <= 0) return {unitsPerSec: 0, unitsPerMin: 0, secPerUnit: 0}
46
+ const unitsPerSec = 1 / secPerUnit
47
+ return {unitsPerSec, unitsPerMin: unitsPerSec * 60, secPerUnit}
28
48
  }
29
49
 
30
- export function calc_gather_energy(
31
- gatherer: ServerContract.Types.gatherer_stats,
32
- duration: number
33
- ): UInt16 {
34
- const energy = Math.floor((duration * gatherer.drain.toNumber()) / PRECISION)
35
- return UInt16.from(energy)
50
+ export function calc_gather_energy(gatherer: GathererStats, duration: number): UInt32 {
51
+ if (duration <= 0) return UInt32.from(0)
52
+ const raw = Math.floor((duration * gatherer.drain.toNumber()) / PRECISION)
53
+ return UInt32.from(Math.min(Math.max(raw, 1000), 4294967295))
36
54
  }
@@ -1,6 +1,5 @@
1
1
  const BASE_HAUL_PENALTY_MILLI = 300
2
2
  const HAULER_EFFICIENCY_DENOM = 10000
3
- const PRECISION = 10000
4
3
 
5
4
  export function computeHaulPenalty(
6
5
  totalThrust: number,
@@ -16,7 +15,3 @@ export function computeHaulPenalty(
16
15
  )
17
16
  return Math.floor((totalThrust * 1000) / penaltyMilli)
18
17
  }
19
-
20
- export function computeHaulerDrain(distance: number, drain: number, haulCount: number): number {
21
- return Math.floor(distance / PRECISION) * drain * haulCount
22
- }
@@ -1,7 +1,6 @@
1
1
  export * from './guards'
2
2
  export * from './movement'
3
3
  export * from './storage'
4
- export * from './loading'
5
4
  export * from './gathering'
6
5
  export * from './crafting'
7
6
  export * from './modules'
@@ -1,13 +1,5 @@
1
- import {
2
- ITEM_CRAFTER_T1,
3
- ITEM_ENGINE_T1,
4
- ITEM_GATHERER_T1,
5
- ITEM_GENERATOR_T1,
6
- ITEM_HAULER_T1,
7
- ITEM_LOADER_T1,
8
- ITEM_STORAGE_T1,
9
- ITEM_WARP_T1,
10
- } from '../data/item-ids'
1
+ import {getItem, tryGetItem} from '../data/catalog'
2
+ import type {EntitySlot} from '../data/recipes-runtime'
11
3
 
12
4
  export const MODULE_ANY = 0
13
5
  export const MODULE_ENGINE = 1
@@ -19,6 +11,9 @@ export const MODULE_CRAFTER = 6
19
11
  export const MODULE_LAUNCHER = 7
20
12
  export const MODULE_STORAGE = 8
21
13
  export const MODULE_HAULER = 9
14
+ export const MODULE_BATTERY = 10
15
+ export const MODULE_BUILDER = 12
16
+ export const MODULE_AUX = 13
22
17
 
23
18
  export interface PackedModule {
24
19
  itemId: number
@@ -31,30 +26,19 @@ export interface ModuleEntry {
31
26
  }
32
27
 
33
28
  export function moduleAccepts(slotType: number, moduleType: number): boolean {
29
+ if (slotType === MODULE_AUX) {
30
+ return (
31
+ moduleType === MODULE_GENERATOR ||
32
+ moduleType === MODULE_ENGINE ||
33
+ moduleType === MODULE_BATTERY
34
+ )
35
+ }
34
36
  return slotType === MODULE_ANY || slotType === moduleType
35
37
  }
36
38
 
37
39
  export function getModuleCapabilityType(itemId: number): number {
38
- switch (itemId) {
39
- case ITEM_ENGINE_T1:
40
- return MODULE_ENGINE
41
- case ITEM_GENERATOR_T1:
42
- return MODULE_GENERATOR
43
- case ITEM_GATHERER_T1:
44
- return MODULE_GATHERER
45
- case ITEM_LOADER_T1:
46
- return MODULE_LOADER
47
- case ITEM_CRAFTER_T1:
48
- return MODULE_CRAFTER
49
- case ITEM_STORAGE_T1:
50
- return MODULE_STORAGE
51
- case ITEM_HAULER_T1:
52
- return MODULE_HAULER
53
- case ITEM_WARP_T1:
54
- return MODULE_WARP
55
- default:
56
- return 0xff
57
- }
40
+ const item = tryGetItem(itemId)
41
+ return item?.type === 'module' && item.moduleType ? moduleSlotTypeToCode(item.moduleType) : 0xff
58
42
  }
59
43
 
60
44
  export function isModuleItem(itemId: number): boolean {
@@ -77,13 +61,26 @@ export function moduleSlotTypeToCode(slotType: string): number {
77
61
  return MODULE_WARP
78
62
  case 'crafter':
79
63
  return MODULE_CRAFTER
64
+ case 'builder':
65
+ return MODULE_BUILDER
80
66
  case 'launcher':
81
67
  return MODULE_LAUNCHER
82
68
  case 'storage':
83
69
  return MODULE_STORAGE
84
70
  case 'hauler':
85
71
  return MODULE_HAULER
72
+ case 'battery':
73
+ return MODULE_BATTERY
74
+ case 'aux':
75
+ return MODULE_AUX
86
76
  default:
87
77
  return MODULE_ANY
88
78
  }
89
79
  }
80
+
81
+ export function slotAcceptsModule(slot: EntitySlot, moduleItemId: number): boolean {
82
+ const moduleType = getModuleCapabilityType(moduleItemId)
83
+ if (moduleType === 0xff) return false
84
+ if (!moduleAccepts(moduleSlotTypeToCode(slot.type), moduleType)) return false
85
+ return getItem(moduleItemId).tier <= slot.maxTier
86
+ }
@@ -9,7 +9,7 @@ export function maxTravelDistance(entity: MovementCapability): UInt32 {
9
9
  }
10
10
 
11
11
  export function calcEnergyUsage(entity: MovementCapability, distance: UInt64): UInt64 {
12
- return distance.dividing(PRECISION).multiplying(entity.engines.drain)
12
+ return distance.multiplying(entity.engines.drain).dividing(PRECISION)
13
13
  }
14
14
 
15
15
  export function hasEnergyForDistance(
@@ -0,0 +1,26 @@
1
+ import {expect, test} from 'bun:test'
2
+ import {UInt32, UInt64} from '@wharfkit/antelope'
3
+ import {hasSpaceForMass, isFull, isFullFromMass} from './storage'
4
+
5
+ test('hasSpaceForMass accepts raw numbers', () => {
6
+ expect(hasSpaceForMass(100, 40, 50)).toBe(true)
7
+ expect(hasSpaceForMass(100, 60, 50)).toBe(false)
8
+ expect(hasSpaceForMass(100, 50, 50)).toBe(true)
9
+ })
10
+
11
+ test('hasSpaceForMass accepts wrapped values', () => {
12
+ expect(hasSpaceForMass(UInt64.from(100), UInt64.from(40), UInt64.from(50))).toBe(true)
13
+ expect(hasSpaceForMass(UInt64.from(100), UInt32.from(60), UInt32.from(50))).toBe(false)
14
+ })
15
+
16
+ test('isFullFromMass accepts raw numbers', () => {
17
+ expect(isFullFromMass(100, 100)).toBe(true)
18
+ expect(isFullFromMass(100, 101)).toBe(true)
19
+ expect(isFullFromMass(100, 99)).toBe(false)
20
+ })
21
+
22
+ test('isFull accepts raw numbers on the entity', () => {
23
+ expect(isFull({capacity: 100, cargomass: 100})).toBe(true)
24
+ expect(isFull({capacity: 100, cargomass: 99})).toBe(false)
25
+ expect(isFull({capacity: UInt32.from(100), cargomass: UInt32.from(120)})).toBe(true)
26
+ })
@@ -1,4 +1,4 @@
1
- import {UInt16, UInt32, UInt64, type UInt64Type} from '@wharfkit/antelope'
1
+ import {UInt16, UInt32, UInt64, type UInt32Type, type UInt64Type} from '@wharfkit/antelope'
2
2
  import {ServerContract} from '../contracts'
3
3
  import type {StorageCapability} from '../types/capabilities'
4
4
  import {getItem} from '../data/catalog'
@@ -9,11 +9,11 @@ export interface HasCargo {
9
9
  }
10
10
 
11
11
  export interface HasCapacity {
12
- capacity: UInt32
12
+ capacity: UInt32Type
13
13
  }
14
14
 
15
15
  export interface HasCargomass {
16
- cargomass: UInt32
16
+ cargomass: UInt32Type
17
17
  }
18
18
 
19
19
  interface MassInput {
@@ -75,15 +75,15 @@ export function hasSpaceForMass(
75
75
  currentMass: UInt64Type,
76
76
  additionalMass: UInt64Type
77
77
  ): boolean {
78
- return UInt64.from(currentMass).adding(additionalMass).lte(capacity)
78
+ return UInt64.from(currentMass).adding(additionalMass).lte(UInt64.from(capacity))
79
79
  }
80
80
 
81
81
  export function isFull(entity: HasCapacity & HasCargomass): boolean {
82
- return UInt64.from(entity.cargomass).gte(entity.capacity)
82
+ return UInt64.from(entity.cargomass).gte(UInt64.from(entity.capacity))
83
83
  }
84
84
 
85
85
  export function isFullFromMass(capacity: UInt64Type, cargoMass: UInt64Type): boolean {
86
- return UInt64.from(cargoMass).gte(capacity)
86
+ return UInt64.from(cargoMass).gte(UInt64.from(capacity))
87
87
  }
88
88
 
89
89
  export interface CargoStack {
@@ -97,7 +97,7 @@ export function cargoItemToStack(item: ServerContract.Types.cargo_item): CargoSt
97
97
  return {
98
98
  item_id: UInt16.from(item.item_id),
99
99
  quantity: UInt32.from(item.quantity),
100
- stats: item.stats,
100
+ stats: UInt64.from(item.stats),
101
101
  modules: item.modules ?? [],
102
102
  }
103
103
  }
@@ -157,3 +157,18 @@ export function removeFromStacks(stacks: CargoStack[], remove: CargoStack): Carg
157
157
  result[idx] = {...target, quantity: remaining}
158
158
  return result
159
159
  }
160
+
161
+ // Tolerant subtraction mirroring the contract's subtract_cargo (projection replay): missing or oversized removal clamps to zero, never throws.
162
+ export function subtractFromStacks(stacks: CargoStack[], remove: CargoStack): CargoStack[] {
163
+ const idx = stacks.findIndex((s) => stackIdentityEqual(s, remove))
164
+ if (idx === -1) {
165
+ return stacks
166
+ }
167
+ const target = stacks[idx]
168
+ if (target.quantity.lte(remove.quantity)) {
169
+ return [...stacks.slice(0, idx), ...stacks.slice(idx + 1)]
170
+ }
171
+ const result = stacks.slice()
172
+ result[idx] = {...target, quantity: UInt32.from(target.quantity.subtracting(remove.quantity))}
173
+ return result
174
+ }