@shipload/sdk 1.0.0-next.39 → 1.0.0-next.40
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/shipload.d.ts +32 -3
- package/lib/shipload.js +167 -15
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +162 -16
- package/lib/shipload.m.js.map +1 -1
- package/package.json +1 -1
- package/src/derivation/index.ts +9 -1
- package/src/derivation/recipe-usage.test.ts +78 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/resources.ts +2 -2
- package/src/derivation/stratum.ts +4 -3
- package/src/derivation/tiers.ts +16 -4
- package/src/derivation/wormhole.ts +21 -0
- package/src/index-module.ts +16 -1
- package/src/travel/route-planner.ts +21 -8
package/package.json
CHANGED
package/src/derivation/index.ts
CHANGED
|
@@ -28,7 +28,15 @@ export {
|
|
|
28
28
|
PLANET_SUBTYPE_INDUSTRIAL,
|
|
29
29
|
} from './resources'
|
|
30
30
|
|
|
31
|
-
export {
|
|
31
|
+
export {
|
|
32
|
+
RESERVE_TIERS,
|
|
33
|
+
TIER_ROLL_MAX,
|
|
34
|
+
tierOfReserve,
|
|
35
|
+
rollTier,
|
|
36
|
+
rollWithinTier,
|
|
37
|
+
RESOURCE_TIER_MULT_TENTHS,
|
|
38
|
+
applyResourceTierMultiplier,
|
|
39
|
+
} from './tiers'
|
|
32
40
|
export type {ReserveTier, TierRange} from './tiers'
|
|
33
41
|
|
|
34
42
|
export {getEffectiveReserve} from './reserve-regen'
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {expect, test} from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
getAllRecipes,
|
|
4
|
+
getRecipeConsumers,
|
|
5
|
+
getComponentDemand,
|
|
6
|
+
getResourceDemand,
|
|
7
|
+
} from './recipe-usage'
|
|
8
|
+
import {
|
|
9
|
+
ITEM_SENSOR,
|
|
10
|
+
ITEM_RESIN,
|
|
11
|
+
ITEM_PLATE,
|
|
12
|
+
ITEM_BEAM,
|
|
13
|
+
ITEM_GATHERER_T1,
|
|
14
|
+
ITEM_CRAFTER_T1,
|
|
15
|
+
ITEM_EXTRACTOR_T1_PACKED,
|
|
16
|
+
ITEM_SHIP_T1_PACKED,
|
|
17
|
+
} from '../data/item-ids'
|
|
18
|
+
|
|
19
|
+
test('getAllRecipes returns the full catalog including the gatherer', () => {
|
|
20
|
+
const all = getAllRecipes()
|
|
21
|
+
expect(all.length).toBeGreaterThan(20)
|
|
22
|
+
expect(all.some((r) => r.outputItemId === ITEM_GATHERER_T1)).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('getRecipeConsumers lists every recipe that consumes Sensor', () => {
|
|
26
|
+
const consumers = getRecipeConsumers(ITEM_SENSOR)
|
|
27
|
+
const ids = consumers.map((c) => c.outputItemId).sort((a, b) => a - b)
|
|
28
|
+
expect(ids).toEqual(
|
|
29
|
+
[ITEM_GATHERER_T1, ITEM_CRAFTER_T1, ITEM_SHIP_T1_PACKED, ITEM_EXTRACTOR_T1_PACKED].sort(
|
|
30
|
+
(a, b) => a - b
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('Sensor feeds the gatherer drain stat', () => {
|
|
36
|
+
const consumers = getRecipeConsumers(ITEM_SENSOR)
|
|
37
|
+
const gatherer = consumers.find((c) => c.outputItemId === ITEM_GATHERER_T1)
|
|
38
|
+
expect(gatherer).toBeDefined()
|
|
39
|
+
const drain = gatherer?.statFlows.find(
|
|
40
|
+
(f) => f.capability === 'Gathering' && f.attribute === 'drain'
|
|
41
|
+
)
|
|
42
|
+
expect(drain).toBeDefined()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('Sensor is a mass-only sink in the Extractor recipe', () => {
|
|
46
|
+
const consumers = getRecipeConsumers(ITEM_SENSOR)
|
|
47
|
+
const extractor = consumers.find((c) => c.outputItemId === ITEM_EXTRACTOR_T1_PACKED)
|
|
48
|
+
expect(extractor).toBeDefined()
|
|
49
|
+
expect(extractor?.statFlows).toHaveLength(0)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('getResourceDemand returns the resource tonnage for a single-resource component', () => {
|
|
53
|
+
expect(getResourceDemand(ITEM_PLATE)).toEqual({ore: 10})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('getResourceDemand traces a dual-resource component to both resources', () => {
|
|
57
|
+
expect(getResourceDemand(ITEM_BEAM)).toEqual({ore: 5, gas: 5})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('getResourceDemand recurses through a module to raw resources', () => {
|
|
61
|
+
// Gatherer = 300 Beam (5 ore + 5 gas each) + 300 Sensor (10 crystal each)
|
|
62
|
+
expect(getResourceDemand(ITEM_GATHERER_T1)).toEqual({
|
|
63
|
+
ore: 1500,
|
|
64
|
+
gas: 1500,
|
|
65
|
+
crystal: 3000,
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('getResourceDemand scales by quantity', () => {
|
|
70
|
+
expect(getResourceDemand(ITEM_PLATE, 3)).toEqual({ore: 30})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('getComponentDemand reports Resin as consumed by exactly one recipe', () => {
|
|
74
|
+
const demand = getComponentDemand()
|
|
75
|
+
const resin = demand.find((d) => d.itemId === ITEM_RESIN)
|
|
76
|
+
expect(resin).toBeDefined()
|
|
77
|
+
expect(resin?.consumerCount).toBe(1)
|
|
78
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import recipes from '../data/recipes.json'
|
|
2
|
+
import {getRecipe, type Recipe} from '../data/recipes-runtime'
|
|
3
|
+
import {getItem} from '../data/catalog'
|
|
4
|
+
import {getStatDefinitions} from './stats'
|
|
5
|
+
import {SLOT_FORMULAS, type SlotConsumerKind} from '../data/capability-formulas'
|
|
6
|
+
import {KIND_TO_ITEM_ID} from './capability-mappings'
|
|
7
|
+
import type {ResourceCategory} from '../types'
|
|
8
|
+
|
|
9
|
+
export function getAllRecipes(): Recipe[] {
|
|
10
|
+
return recipes as unknown as Recipe[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ResourceDemand = Partial<Record<ResourceCategory, number>>
|
|
14
|
+
|
|
15
|
+
function accumulateResourceDemand(itemId: number, quantity: number, out: ResourceDemand): void {
|
|
16
|
+
const item = getItem(itemId)
|
|
17
|
+
if (item.type === 'resource' && item.category) {
|
|
18
|
+
out[item.category] = (out[item.category] ?? 0) + quantity
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
const recipe = getRecipe(itemId)
|
|
22
|
+
if (!recipe) return
|
|
23
|
+
for (const input of recipe.inputs) {
|
|
24
|
+
accumulateResourceDemand(input.itemId, input.quantity * quantity, out)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Raw-resource tonnage to craft `quantity` of an item, tracing recipes to the resource leaves.
|
|
29
|
+
export function getResourceDemand(itemId: number, quantity = 1): ResourceDemand {
|
|
30
|
+
const out: ResourceDemand = {}
|
|
31
|
+
accumulateResourceDemand(itemId, quantity, out)
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ITEM_ID_TO_KIND = new Map<number, SlotConsumerKind>()
|
|
36
|
+
for (const [kind, itemId] of Object.entries(KIND_TO_ITEM_ID) as [SlotConsumerKind, number][]) {
|
|
37
|
+
ITEM_ID_TO_KIND.set(itemId, kind)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Traces a stat index down to the raw category stat label it carries (Sensor stat 0 → "Conductivity").
|
|
41
|
+
function resolveComponentStatLabel(itemId: number, statIndex: number): string | undefined {
|
|
42
|
+
let item: ReturnType<typeof getItem>
|
|
43
|
+
try {
|
|
44
|
+
item = getItem(itemId)
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
if (item.type === 'resource' && item.category) {
|
|
49
|
+
return getStatDefinitions(item.category)[statIndex]?.label
|
|
50
|
+
}
|
|
51
|
+
const recipe = getRecipe(itemId)
|
|
52
|
+
const slot = recipe?.statSlots[statIndex]
|
|
53
|
+
const source = slot?.sources[0]
|
|
54
|
+
if (!recipe || !source) return undefined
|
|
55
|
+
const input = recipe.inputs[source.inputIndex]
|
|
56
|
+
if (!input) return undefined
|
|
57
|
+
return resolveComponentStatLabel(input.itemId, source.statIndex)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface StatFlow {
|
|
61
|
+
slotIndex: number
|
|
62
|
+
capability?: string
|
|
63
|
+
attribute?: string
|
|
64
|
+
sourceStatIndex: number
|
|
65
|
+
sourceStatLabel?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface RecipeConsumer {
|
|
69
|
+
outputItemId: number
|
|
70
|
+
quantity: number
|
|
71
|
+
statFlows: StatFlow[]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Every recipe that consumes `componentItemId`, with how its stats flow through. */
|
|
75
|
+
export function getRecipeConsumers(componentItemId: number): RecipeConsumer[] {
|
|
76
|
+
const out: RecipeConsumer[] = []
|
|
77
|
+
for (const recipe of getAllRecipes()) {
|
|
78
|
+
for (let inputIndex = 0; inputIndex < recipe.inputs.length; inputIndex++) {
|
|
79
|
+
if (recipe.inputs[inputIndex].itemId !== componentItemId) continue
|
|
80
|
+
const kind = ITEM_ID_TO_KIND.get(recipe.outputItemId)
|
|
81
|
+
const formulas = kind ? SLOT_FORMULAS[kind] : undefined
|
|
82
|
+
const statFlows: StatFlow[] = []
|
|
83
|
+
for (let slotIndex = 0; slotIndex < recipe.statSlots.length; slotIndex++) {
|
|
84
|
+
for (const source of recipe.statSlots[slotIndex].sources) {
|
|
85
|
+
if (source.inputIndex !== inputIndex) continue
|
|
86
|
+
const consumer = formulas?.[slotIndex]
|
|
87
|
+
statFlows.push({
|
|
88
|
+
slotIndex,
|
|
89
|
+
capability: consumer?.capability,
|
|
90
|
+
attribute: consumer?.attribute,
|
|
91
|
+
sourceStatIndex: source.statIndex,
|
|
92
|
+
sourceStatLabel: resolveComponentStatLabel(
|
|
93
|
+
componentItemId,
|
|
94
|
+
source.statIndex
|
|
95
|
+
),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
out.push({
|
|
100
|
+
outputItemId: recipe.outputItemId,
|
|
101
|
+
quantity: recipe.inputs[inputIndex].quantity,
|
|
102
|
+
statFlows,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface DemandRow {
|
|
110
|
+
itemId: number
|
|
111
|
+
consumerCount: number
|
|
112
|
+
statSourceCount: number
|
|
113
|
+
sinkOnlyCount: number
|
|
114
|
+
consumers: number[]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Demand tally for every item consumed as a recipe input, ascending by usage. */
|
|
118
|
+
export function getComponentDemand(): DemandRow[] {
|
|
119
|
+
const inputIds = new Set<number>()
|
|
120
|
+
for (const recipe of getAllRecipes()) {
|
|
121
|
+
for (const input of recipe.inputs) inputIds.add(input.itemId)
|
|
122
|
+
}
|
|
123
|
+
const rows: DemandRow[] = []
|
|
124
|
+
for (const itemId of inputIds) {
|
|
125
|
+
const consumers = getRecipeConsumers(itemId)
|
|
126
|
+
let statSourceCount = 0
|
|
127
|
+
let sinkOnlyCount = 0
|
|
128
|
+
for (const c of consumers) {
|
|
129
|
+
if (c.statFlows.length > 0) statSourceCount++
|
|
130
|
+
else sinkOnlyCount++
|
|
131
|
+
}
|
|
132
|
+
rows.push({
|
|
133
|
+
itemId,
|
|
134
|
+
consumerCount: consumers.length,
|
|
135
|
+
statSourceCount,
|
|
136
|
+
sinkOnlyCount,
|
|
137
|
+
consumers: consumers.map((c) => c.outputItemId),
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
return rows.sort((a, b) => a.consumerCount - b.consumerCount || a.itemId - b.itemId)
|
|
141
|
+
}
|
|
@@ -15,8 +15,8 @@ export const DEPTH_THRESHOLD_T10 = 63000
|
|
|
15
15
|
export const LOCATION_MIN_DEPTH = 500
|
|
16
16
|
export const LOCATION_MAX_DEPTH = 65535
|
|
17
17
|
|
|
18
|
-
export const YIELD_FRACTION_SHALLOW = 0.
|
|
19
|
-
export const YIELD_FRACTION_DEEP = 0.
|
|
18
|
+
export const YIELD_FRACTION_SHALLOW = 0.005
|
|
19
|
+
export const YIELD_FRACTION_DEEP = 0.001
|
|
20
20
|
|
|
21
21
|
export function yieldThresholdAt(stratum: number): number {
|
|
22
22
|
const clamped = stratum > 65535 ? 65535 : stratum
|
|
@@ -3,7 +3,7 @@ import {hash512} from '../utils/hash'
|
|
|
3
3
|
import {Coordinates, type CoordinatesType} from '../types'
|
|
4
4
|
import {getItem} from '../data/catalog'
|
|
5
5
|
import {getEligibleResources, getResourceWeight, yieldThresholdAt} from './resources'
|
|
6
|
-
import {RESERVE_TIERS, rollTier, rollWithinTier} from './tiers'
|
|
6
|
+
import {RESERVE_TIERS, rollTier, rollWithinTier, applyResourceTierMultiplier} from './tiers'
|
|
7
7
|
|
|
8
8
|
export interface StratumInfo {
|
|
9
9
|
itemId: number
|
|
@@ -63,8 +63,9 @@ export function deriveStratum(
|
|
|
63
63
|
const tierRoll = ((bytes[18] << 8) | bytes[19]) >>> 0
|
|
64
64
|
const withinRoll = ((bytes[20] << 8) | bytes[21]) >>> 0
|
|
65
65
|
const tier = rollTier(tierRoll, stratum)
|
|
66
|
-
const
|
|
67
|
-
const
|
|
66
|
+
const selected = getItem(selectedItemId)
|
|
67
|
+
const baseReserve = rollWithinTier(withinRoll, RESERVE_TIERS[tier], selected.mass)
|
|
68
|
+
const reserve = applyResourceTierMultiplier(baseReserve, selected.tier)
|
|
68
69
|
|
|
69
70
|
const seedBigInt =
|
|
70
71
|
(BigInt(bytes[8]) << 56n) |
|
package/src/derivation/tiers.ts
CHANGED
|
@@ -60,14 +60,26 @@ export function rollWithinTier(
|
|
|
60
60
|
return Math.max(1, Math.floor(depositMass / resourceUnitMass))
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Must mirror the contract tier-multiplier table in tiers.hpp byte-for-byte; values are in tenths (T1..T10).
|
|
64
|
+
export const RESOURCE_TIER_MULT_TENTHS = [200, 154, 118, 91, 70, 54, 41, 32, 24, 19] as const
|
|
65
|
+
|
|
66
|
+
export function applyResourceTierMultiplier(units: number, resourceTier: number): number {
|
|
67
|
+
const idx = resourceTier < 1 ? 0 : resourceTier > 10 ? 9 : resourceTier - 1
|
|
68
|
+
const scaled = Math.floor((units * RESOURCE_TIER_MULT_TENTHS[idx]) / 10)
|
|
69
|
+
return scaled > 0 ? scaled : 1
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
const RESERVE_TIER_ENTRIES = Object.entries(RESERVE_TIERS) as Array<[ReserveTier, TierRange]>
|
|
64
73
|
|
|
65
74
|
export function tierOfReserve(reserve: number, itemId: number): ReserveTier | null {
|
|
66
75
|
if (reserve <= 0) return null
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
const
|
|
76
|
+
const item = getItem(itemId)
|
|
77
|
+
if (item.mass <= 0) return null
|
|
78
|
+
// Reverse the resource-tier multiplier so bands read relative to the resource tier.
|
|
79
|
+
const idx = item.tier < 1 ? 0 : item.tier > 10 ? 9 : item.tier - 1
|
|
80
|
+
const baseReserve = (reserve * 10) / RESOURCE_TIER_MULT_TENTHS[idx]
|
|
81
|
+
const impliedMassLow = baseReserve * item.mass
|
|
82
|
+
const impliedMassHigh = impliedMassLow + item.mass
|
|
71
83
|
for (const [tier, range] of RESERVE_TIER_ENTRIES) {
|
|
72
84
|
if (impliedMassHigh > range.min && impliedMassLow <= range.max) return tier
|
|
73
85
|
}
|
|
@@ -103,6 +103,27 @@ export function wormholeAt(
|
|
|
103
103
|
if (!w || w.A.x !== x || w.A.y !== y) return null
|
|
104
104
|
return w.B
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
// Wormhole mouths (the local A endpoint) within reachTiles of (x,y); regions are RSIZE-wide so only a few overlap.
|
|
108
|
+
export function nearbyWormholes(
|
|
109
|
+
seed: Checksum256Type,
|
|
110
|
+
x: number,
|
|
111
|
+
y: number,
|
|
112
|
+
reachTiles: number
|
|
113
|
+
): {x: number; y: number}[] {
|
|
114
|
+
const min = regionOf(x - reachTiles, y - reachTiles)
|
|
115
|
+
const max = regionOf(x + reachTiles, y + reachTiles)
|
|
116
|
+
const out: {x: number; y: number}[] = []
|
|
117
|
+
for (let rx = min.rx; rx <= max.rx; rx++) {
|
|
118
|
+
for (let ry = min.ry; ry <= max.ry; ry++) {
|
|
119
|
+
const w = wormholeOfRegion(seed, {rx, ry})
|
|
120
|
+
if (!w) continue
|
|
121
|
+
if (w.A.x === x && w.A.y === y) continue
|
|
122
|
+
if (dist({x, y}, w.A) <= reachTiles) out.push(w.A)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return out
|
|
126
|
+
}
|
|
106
127
|
export function isValidWormholePair(
|
|
107
128
|
seed: Checksum256Type,
|
|
108
129
|
ax: number,
|
package/src/index-module.ts
CHANGED
|
@@ -132,7 +132,15 @@ export {
|
|
|
132
132
|
|
|
133
133
|
export type {StratumInfo, ResourceStats, DerivedStratum} from './derivation'
|
|
134
134
|
|
|
135
|
-
export {
|
|
135
|
+
export {
|
|
136
|
+
RESERVE_TIERS,
|
|
137
|
+
TIER_ROLL_MAX,
|
|
138
|
+
tierOfReserve,
|
|
139
|
+
rollTier,
|
|
140
|
+
rollWithinTier,
|
|
141
|
+
RESOURCE_TIER_MULT_TENTHS,
|
|
142
|
+
applyResourceTierMultiplier,
|
|
143
|
+
} from './derivation'
|
|
136
144
|
export type {ReserveTier, TierRange} from './derivation'
|
|
137
145
|
|
|
138
146
|
export {getEffectiveReserve} from './derivation'
|
|
@@ -348,6 +356,13 @@ export {
|
|
|
348
356
|
} from './derivation/capability-mappings'
|
|
349
357
|
export {SLOT_FORMULAS} from './data/capability-formulas'
|
|
350
358
|
export type {SlotConsumer, SlotConsumerKind} from './data/capability-formulas'
|
|
359
|
+
export {
|
|
360
|
+
getAllRecipes,
|
|
361
|
+
getRecipeConsumers,
|
|
362
|
+
getComponentDemand,
|
|
363
|
+
getResourceDemand,
|
|
364
|
+
} from './derivation/recipe-usage'
|
|
365
|
+
export type {StatFlow, RecipeConsumer, DemandRow, ResourceDemand} from './derivation/recipe-usage'
|
|
351
366
|
|
|
352
367
|
export {
|
|
353
368
|
encodeStats,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {distanceBetweenPoints, findNearbyPlanets} from './travel'
|
|
2
2
|
import {hasSystem} from '../utils/system'
|
|
3
|
+
import {nearbyWormholes, wormholeAt} from '../derivation/wormhole'
|
|
3
4
|
import {PRECISION} from '../types'
|
|
4
5
|
import {Checksum256, type Checksum256Type} from '@wharfkit/antelope'
|
|
5
6
|
|
|
@@ -170,14 +171,26 @@ function reconstruct(cameFrom: Map<string, Coord>, origin: Coord, dest: Coord):
|
|
|
170
171
|
|
|
171
172
|
export function sdkSystemGraph(seed: Checksum256Type): SystemGraph {
|
|
172
173
|
const s = Checksum256.from(seed)
|
|
174
|
+
// Travelable nodes mirror the contract's is_travelable: systems plus wormhole mouths.
|
|
173
175
|
return {
|
|
174
|
-
hasSystem: (c) => hasSystem(s, {x: c.x, y: c.y}),
|
|
175
|
-
nearby: (c, reachTiles) =>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
176
|
+
hasSystem: (c) => hasSystem(s, {x: c.x, y: c.y}) || wormholeAt(s, c.x, c.y) !== null,
|
|
177
|
+
nearby: (c, reachTiles) => {
|
|
178
|
+
const seen = new Set<string>([`${c.x},${c.y}`])
|
|
179
|
+
const out: Neighbor[] = []
|
|
180
|
+
for (const d of findNearbyPlanets(s, {x: c.x, y: c.y}, reachTiles * PRECISION)) {
|
|
181
|
+
const coord = {x: Number(d.destination.x), y: Number(d.destination.y)}
|
|
182
|
+
const k = `${coord.x},${coord.y}`
|
|
183
|
+
if (seen.has(k)) continue
|
|
184
|
+
seen.add(k)
|
|
185
|
+
out.push({coord, dist: Number(d.distance) / PRECISION})
|
|
186
|
+
}
|
|
187
|
+
for (const coord of nearbyWormholes(s, c.x, c.y, reachTiles)) {
|
|
188
|
+
const k = `${coord.x},${coord.y}`
|
|
189
|
+
if (seen.has(k)) continue
|
|
190
|
+
seen.add(k)
|
|
191
|
+
out.push({coord, dist: Math.hypot(coord.x - c.x, coord.y - c.y)})
|
|
192
|
+
}
|
|
193
|
+
return out
|
|
194
|
+
},
|
|
182
195
|
}
|
|
183
196
|
}
|