@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.
- package/lib/scan.d.ts +52 -0
- package/lib/scan.js +162 -0
- package/lib/scan.js.map +1 -0
- package/lib/scan.m.js +152 -0
- package/lib/scan.m.js.map +1 -0
- package/lib/shipload.d.ts +5696 -1503
- package/lib/shipload.js +20294 -5200
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +20371 -5683
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +1648 -0
- package/lib/testing.js +6980 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +6974 -0
- package/lib/testing.m.js.map +1 -0
- package/package.json +20 -2
- package/src/capabilities/craftable.test.ts +82 -0
- package/src/capabilities/craftable.ts +82 -0
- package/src/capabilities/crafting.test.ts +80 -0
- package/src/capabilities/crafting.ts +50 -8
- package/src/capabilities/gathering.test.ts +29 -0
- package/src/capabilities/gathering.ts +38 -20
- package/src/capabilities/hauling.ts +0 -5
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +27 -30
- package/src/capabilities/movement.ts +1 -1
- package/src/capabilities/storage.test.ts +26 -0
- package/src/capabilities/storage.ts +22 -7
- package/src/contracts/fund.ts +306 -0
- package/src/contracts/platform.ts +312 -3
- package/src/contracts/server.ts +2661 -466
- package/src/contracts/token.ts +170 -0
- package/src/contracts.ts +2 -0
- package/src/coordinates/address.ts +88 -0
- package/src/coordinates/constants.test.ts +15 -0
- package/src/coordinates/constants.ts +23 -0
- package/src/coordinates/index.ts +15 -0
- package/src/coordinates/memo.test.ts +47 -0
- package/src/coordinates/memo.ts +20 -0
- package/src/coordinates/permutation.ts +77 -0
- package/src/coordinates/regions.ts +48 -0
- package/src/coordinates/sectors.ts +115 -0
- package/src/data/capabilities.ts +49 -18
- package/src/data/capability-formulas.ts +26 -14
- package/src/data/catalog.test.ts +19 -0
- package/src/data/catalog.ts +13 -3
- package/src/data/charters.json +396 -0
- package/src/data/citizenry-adjectives.json +42 -0
- package/src/data/citizenry-nouns.json +50 -0
- package/src/data/colors.ts +14 -32
- package/src/data/entities.json +421 -15
- package/src/data/item-ids.ts +53 -13
- package/src/data/items.json +330 -77
- package/src/data/kind-registry.json +212 -0
- package/src/data/kind-registry.ts +192 -0
- package/src/data/metadata.ts +312 -51
- package/src/data/recipes-runtime.ts +6 -23
- package/src/data/recipes.json +2173 -173
- package/src/data/tiers.ts +9 -6
- package/src/derivation/build-methods.test.ts +71 -0
- package/src/derivation/build-methods.ts +50 -0
- package/src/derivation/capabilities.test.ts +348 -0
- package/src/derivation/capabilities.ts +707 -0
- package/src/derivation/capability-mappings.ts +60 -16
- package/src/derivation/crafting.test.ts +17 -0
- package/src/derivation/crafting.ts +57 -34
- package/src/derivation/index.ts +27 -2
- package/src/derivation/recipe-usage.test.ts +88 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +9 -1
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +72 -0
- package/src/derivation/stars.test.ts +67 -0
- package/src/derivation/stars.ts +25 -0
- package/src/derivation/stat-scaling.ts +12 -0
- package/src/derivation/stats.ts +6 -6
- package/src/derivation/strata.ts +5 -3
- package/src/derivation/stratum.ts +26 -22
- package/src/derivation/tiers.ts +40 -7
- package/src/derivation/upgrades.ts +14 -0
- package/src/derivation/wormhole.ts +154 -0
- package/src/entities/entity.ts +102 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +144 -136
- package/src/entities/slot-multiplier.ts +43 -0
- package/src/entity/hydrate.test.ts +156 -0
- package/src/entity/hydrate.ts +86 -0
- package/src/errors.ts +21 -16
- package/src/format.ts +42 -9
- package/src/index-module.ts +411 -32
- package/src/influence/ballot.test.ts +103 -0
- package/src/influence/ballot.ts +98 -0
- package/src/influence/categories.ts +32 -0
- package/src/influence/charter-metadata.test.ts +136 -0
- package/src/influence/charter-metadata.ts +236 -0
- package/src/influence/charters.test.ts +112 -0
- package/src/influence/charters.ts +150 -0
- package/src/influence/citizenry.ts +38 -0
- package/src/influence/civic-shuttle.test.ts +202 -0
- package/src/influence/civic-shuttle.ts +142 -0
- package/src/influence/civic.test.ts +287 -0
- package/src/influence/civic.ts +269 -0
- package/src/influence/constants.ts +84 -0
- package/src/influence/decay.ts +20 -0
- package/src/influence/decomp.ts +95 -0
- package/src/influence/demand.ts +106 -0
- package/src/influence/duration.ts +101 -0
- package/src/influence/index.ts +118 -0
- package/src/influence/influence.test.ts +450 -0
- package/src/influence/quality.ts +25 -0
- package/src/influence/valuation.ts +154 -0
- package/src/managers/actions.ts +940 -93
- package/src/managers/balances.test.ts +161 -0
- package/src/managers/balances.ts +93 -0
- package/src/managers/base.ts +10 -2
- package/src/managers/cluster.test.ts +39 -0
- package/src/managers/cluster.ts +53 -0
- package/src/managers/construction-types.ts +80 -0
- package/src/managers/construction.ts +453 -0
- package/src/managers/context.ts +58 -1
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/entities.ts +35 -63
- package/src/managers/epochs.ts +72 -1
- package/src/managers/index.ts +38 -1
- package/src/managers/influence.test.ts +56 -0
- package/src/managers/influence.ts +425 -0
- package/src/managers/jobs.test.ts +80 -0
- package/src/managers/jobs.ts +54 -0
- package/src/managers/locations.ts +25 -29
- package/src/managers/nft.test.ts +21 -0
- package/src/managers/nft.ts +110 -0
- package/src/managers/players.ts +25 -0
- package/src/managers/plot.ts +122 -0
- package/src/nft/atomicassets.abi.json +1342 -0
- package/src/nft/atomicassets.ts +237 -0
- package/src/nft/atomicdata.ts +130 -0
- package/src/nft/buildImmutableData.ts +388 -0
- package/src/nft/description.ts +222 -100
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +296 -0
- package/src/planner/planner.test.ts +459 -0
- package/src/receipts.ts +22 -0
- package/src/resolution/describe-module.ts +43 -25
- package/src/resolution/display-name.ts +39 -11
- package/src/resolution/resolve-item.test.ts +48 -0
- package/src/resolution/resolve-item.ts +112 -58
- package/src/scan/index.ts +244 -0
- package/src/scan/scan-wasm.base64.ts +2 -0
- package/src/scheduling/accessor.ts +65 -23
- package/src/scheduling/availability.test.ts +320 -0
- package/src/scheduling/availability.ts +251 -0
- package/src/scheduling/cancel.test.ts +550 -0
- package/src/scheduling/cancel.ts +254 -0
- package/src/scheduling/cluster-stock.test.ts +31 -0
- package/src/scheduling/cluster-stock.ts +15 -0
- package/src/scheduling/energy.ts +47 -0
- package/src/scheduling/epoch.ts +1 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/jobs.test.ts +49 -0
- package/src/scheduling/jobs.ts +137 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +250 -0
- package/src/scheduling/lanes.ts +227 -0
- package/src/scheduling/projection.ts +265 -149
- package/src/scheduling/schedule.test.ts +163 -0
- package/src/scheduling/schedule.ts +282 -116
- package/src/scheduling/task-cargo.test.ts +110 -0
- package/src/scheduling/task-cargo.ts +62 -0
- package/src/scheduling/task-effects.test.ts +35 -0
- package/src/scheduling/task-effects.ts +34 -0
- package/src/scheduling/unwrap.test.ts +86 -0
- package/src/scheduling/unwrap.ts +190 -0
- package/src/shipload.ts +63 -4
- package/src/subscriptions/manager.cluster.test.ts +51 -0
- package/src/subscriptions/manager.ts +245 -172
- package/src/subscriptions/mappers.ts +5 -8
- package/src/subscriptions/types.ts +28 -3
- package/src/testing/catalog-hash.ts +19 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/projection-parity.ts +167 -0
- package/src/travel/reach.test.ts +86 -0
- package/src/travel/reach.ts +34 -0
- package/src/travel/route-build.test.ts +63 -0
- package/src/travel/route-build.ts +60 -0
- package/src/travel/route-planner.ts +288 -0
- package/src/travel/route-simulator.ts +170 -0
- package/src/travel/travel.ts +190 -123
- package/src/types/capabilities.ts +37 -8
- package/src/types/entity.ts +3 -3
- package/src/types/index.ts +0 -1
- package/src/types.ts +81 -23
- package/src/utils/cargo.test.ts +14 -0
- package/src/utils/cargo.ts +2 -0
- package/src/utils/display-name.ts +70 -0
- package/src/utils/hash.ts +22 -2
- package/src/utils/system.ts +24 -5
- package/src/capabilities/loading.ts +0 -8
- package/src/entities/container.ts +0 -123
- package/src/entities/ship-deploy.ts +0 -295
- package/src/entities/ship.ts +0 -221
- package/src/entities/warehouse.ts +0 -127
- package/src/types/entity-traits.ts +0 -69
|
@@ -0,0 +1,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 {
|
|
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:
|
|
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(
|
|
17
|
+
const duration = Math.floor((totalInputMass * MASS_STAT_SCALE) / speed)
|
|
18
|
+
return UInt32.from(duration + 1)
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export
|
|
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
|
|
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 {
|
|
2
|
-
import type {
|
|
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
|
-
|
|
10
|
-
gatherer:
|
|
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
|
-
):
|
|
15
|
+
): number {
|
|
16
16
|
const yieldValue = gatherer.yield.toNumber()
|
|
17
|
-
const speed = gatherer.speed.toNumber()
|
|
18
17
|
|
|
19
|
-
if (yieldValue === 0 ||
|
|
18
|
+
if (yieldValue === 0 || richness === 0) return 0
|
|
20
19
|
|
|
21
|
-
const massFactor =
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
duration
|
|
33
|
-
)
|
|
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,13 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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.
|
|
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:
|
|
12
|
+
capacity: UInt32Type
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface HasCargomass {
|
|
16
|
-
cargomass:
|
|
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
|
+
}
|