@jgengine/core 0.8.0 → 0.9.0
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/CHANGELOG.md +16 -0
- package/dist/ai/flock.d.ts +36 -0
- package/dist/ai/flock.js +182 -0
- package/dist/ai/groupAssist.d.ts +19 -0
- package/dist/ai/groupAssist.js +59 -0
- package/dist/ai/laneSelect.d.ts +21 -0
- package/dist/ai/laneSelect.js +37 -0
- package/dist/ai/mobBrain.d.ts +68 -0
- package/dist/ai/mobBrain.js +115 -0
- package/dist/ai/spawnDirector.js +1 -1
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/combat/abilityKit.d.ts +17 -2
- package/dist/combat/abilityKit.js +62 -13
- package/dist/combat/castRunner.d.ts +40 -0
- package/dist/combat/castRunner.js +56 -0
- package/dist/combat/comboPoints.d.ts +14 -0
- package/dist/combat/comboPoints.js +52 -0
- package/dist/combat/defensiveWindow.d.ts +1 -1
- package/dist/combat/defensiveWindow.js +5 -3
- package/dist/combat/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +201 -129
- package/dist/combat/resistance.d.ts +16 -4
- package/dist/combat/resistance.js +43 -7
- package/dist/combat/resourcePool.d.ts +20 -0
- package/dist/combat/resourcePool.js +55 -0
- package/dist/combat/shotOrigin.d.ts +40 -0
- package/dist/combat/shotOrigin.js +57 -0
- package/dist/combat/telegraph.d.ts +25 -0
- package/dist/combat/telegraph.js +47 -0
- package/dist/crafting/recipe.js +4 -1
- package/dist/devtools/devtools.d.ts +72 -24
- package/dist/devtools/devtools.js +449 -101
- package/dist/devtools/transformTunables.js +3 -3
- package/dist/devtools/tunableSchema.d.ts +140 -0
- package/dist/devtools/tunableSchema.js +476 -0
- package/dist/faction/factions.d.ts +33 -0
- package/dist/faction/factions.js +76 -0
- package/dist/faction/reputation.d.ts +32 -0
- package/dist/faction/reputation.js +82 -0
- package/dist/game/chat.d.ts +2 -0
- package/dist/game/chat.js +9 -1
- package/dist/game/chatFilter.d.ts +15 -0
- package/dist/game/chatFilter.js +55 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +2 -0
- package/dist/game/events.d.ts +10 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +50 -1
- package/dist/game/playableGame.js +1 -1
- package/dist/game/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +39 -1
- package/dist/game/race.js +96 -11
- package/dist/game/recordBook.d.ts +29 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/input/axisInput.d.ts +26 -3
- package/dist/input/axisInput.js +44 -4
- package/dist/input/bindingOverrides.d.ts +25 -0
- package/dist/input/bindingOverrides.js +95 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +6 -0
- package/dist/input/touchScheme.js +45 -7
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +23 -1
- package/dist/movement/dash.d.ts +3 -0
- package/dist/movement/dash.js +19 -5
- package/dist/movement/glideModel.d.ts +44 -0
- package/dist/movement/glideModel.js +54 -0
- package/dist/movement/grappleSwing.d.ts +34 -0
- package/dist/movement/grappleSwing.js +56 -0
- package/dist/movement/leaderTrail.d.ts +29 -0
- package/dist/movement/leaderTrail.js +67 -0
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- package/dist/multiplayer/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/presenceModel.d.ts +14 -2
- package/dist/multiplayer/presenceModel.js +15 -3
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -0
- package/dist/nav/navGrid.js +30 -4
- package/dist/nav/railGraph.d.ts +66 -0
- package/dist/nav/railGraph.js +129 -0
- package/dist/nav/timetable.d.ts +41 -0
- package/dist/nav/timetable.js +94 -0
- package/dist/physics/buoyancy.d.ts +6 -0
- package/dist/physics/buoyancy.js +14 -0
- package/dist/physics/flowTube.d.ts +32 -0
- package/dist/physics/flowTube.js +64 -0
- package/dist/physics/forceVolume.d.ts +27 -0
- package/dist/physics/forceVolume.js +38 -0
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +13 -6
- package/dist/physics/physicsWorld.js +169 -40
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/vehicleBody.js +2 -1
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/runtime/cameraDirector.d.ts +6 -1
- package/dist/runtime/cameraDirector.js +6 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +21 -0
- package/dist/runtime/gameContext.js +184 -9
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +9 -1
- package/dist/runtime/hostPersistence.js +23 -8
- package/dist/runtime/inputSnapshot.d.ts +5 -0
- package/dist/runtime/inputSnapshot.js +5 -0
- package/dist/runtime/motionIntents.d.ts +14 -6
- package/dist/runtime/motionIntents.js +20 -2
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/visibility.d.ts +74 -0
- package/dist/runtime/visibility.js +127 -0
- package/dist/scene/colliders.d.ts +44 -0
- package/dist/scene/colliders.js +77 -0
- package/dist/scene/entityStore.d.ts +22 -5
- package/dist/scene/entityStore.js +59 -12
- package/dist/scene/objectQuery.d.ts +13 -2
- package/dist/scene/objectQuery.js +34 -2
- package/dist/scene/objectStore.d.ts +2 -0
- package/dist/scene/objectStore.js +31 -12
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +11 -16
- package/dist/scene/spatial.js +99 -38
- package/dist/sensor/recordingBuffer.d.ts +6 -0
- package/dist/sensor/recordingBuffer.js +7 -0
- package/dist/sensor/replayLoop.d.ts +39 -0
- package/dist/sensor/replayLoop.js +64 -0
- package/dist/sensor/visionCone.d.ts +31 -0
- package/dist/sensor/visionCone.js +72 -0
- package/dist/session/extraction.d.ts +2 -2
- package/dist/session/extraction.js +13 -2
- package/dist/settings/settingsModel.d.ts +97 -0
- package/dist/settings/settingsModel.js +100 -0
- package/dist/stats/statModifiers.d.ts +5 -2
- package/dist/stats/statModifiers.js +32 -14
- package/dist/store/observableKeyedStore.js +8 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/time/beatClock.d.ts +18 -0
- package/dist/time/beatClock.js +24 -0
- package/dist/time/idleProgress.d.ts +29 -0
- package/dist/time/idleProgress.js +31 -0
- package/dist/time/serverTick.d.ts +7 -1
- package/dist/time/serverTick.js +27 -4
- package/dist/time/stateSchedule.d.ts +57 -0
- package/dist/time/stateSchedule.js +121 -0
- package/dist/turn/commit.d.ts +2 -2
- package/dist/turn/commit.js +2 -2
- package/dist/ui/hudLayout.d.ts +58 -0
- package/dist/ui/hudLayout.js +207 -0
- package/dist/ui/hudScale.d.ts +44 -0
- package/dist/ui/hudScale.js +55 -0
- package/dist/visibility/assetStreaming.d.ts +66 -0
- package/dist/visibility/assetStreaming.js +201 -0
- package/dist/visibility/bounds.d.ts +66 -0
- package/dist/visibility/bounds.js +113 -0
- package/dist/visibility/camera.d.ts +21 -0
- package/dist/visibility/camera.js +3 -0
- package/dist/visibility/config.d.ts +21 -0
- package/dist/visibility/config.js +1 -0
- package/dist/visibility/diagnostics.d.ts +26 -0
- package/dist/visibility/diagnostics.js +36 -0
- package/dist/visibility/distance.d.ts +11 -0
- package/dist/visibility/distance.js +30 -0
- package/dist/visibility/frustum.d.ts +47 -0
- package/dist/visibility/frustum.js +186 -0
- package/dist/visibility/occlusion.d.ts +36 -0
- package/dist/visibility/occlusion.js +45 -0
- package/dist/visibility/settings.d.ts +75 -0
- package/dist/visibility/settings.js +49 -0
- package/dist/visibility/simulationCulling.d.ts +34 -0
- package/dist/visibility/simulationCulling.js +36 -0
- package/dist/visibility/spatialIndex.d.ts +45 -0
- package/dist/visibility/spatialIndex.js +198 -0
- package/dist/visibility/visibilitySystem.d.ts +90 -0
- package/dist/visibility/visibilitySystem.js +257 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/buildings.js +152 -0
- package/dist/world/carve.d.ts +7 -2
- package/dist/world/carve.js +17 -7
- package/dist/world/cellStates.d.ts +45 -0
- package/dist/world/cellStates.js +103 -0
- package/dist/world/environmentSummary.d.ts +10 -2
- package/dist/world/environmentSummary.js +11 -2
- package/dist/world/features.d.ts +56 -8
- package/dist/world/features.js +24 -3
- package/dist/world/gridInstances.d.ts +7 -8
- package/dist/world/gridInstances.js +145 -7
- package/dist/world/lod.d.ts +17 -0
- package/dist/world/lod.js +66 -0
- package/dist/world/mapLayers.d.ts +60 -0
- package/dist/world/mapLayers.js +44 -0
- package/dist/world/massing.d.ts +45 -0
- package/dist/world/massing.js +581 -0
- package/dist/world/minimap.d.ts +13 -1
- package/dist/world/minimap.js +21 -2
- package/dist/world/terrain.d.ts +42 -3
- package/dist/world/terrain.js +197 -16
- package/dist/world/water.d.ts +7 -0
- package/dist/world/water.js +19 -4
- package/dist/world/weather.d.ts +3 -18
- package/dist/world/weather.js +4 -17
- package/dist/world/windZones.d.ts +47 -0
- package/dist/world/windZones.js +59 -0
- package/llms.txt +2585 -1704
- package/package.json +12 -2
package/dist/world/terrain.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TerrainEnvironmentConfig, TerrainEnvironmentDescriptor, WorldBounds, WorldFeature } from "./features.js";
|
|
1
|
+
import type { EnvironmentWorldFeature, TerrainEnvironmentConfig, TerrainEnvironmentDescriptor, TerrainIslandDescriptor, TerrainMaterial, WorldBounds, WorldFeature } from "./features.js";
|
|
2
2
|
export type TerrainNormal = readonly [number, number, number];
|
|
3
3
|
export interface TerrainField {
|
|
4
4
|
sampleHeight(x: number, z: number): number;
|
|
@@ -47,14 +47,53 @@ export interface ArenaFieldConfig {
|
|
|
47
47
|
export declare const ARENA_WATER_LEVEL = -0.9;
|
|
48
48
|
export declare function arenaField(config?: ArenaFieldConfig): TerrainField;
|
|
49
49
|
export declare function groundFieldFor(world?: WorldFeature): TerrainField;
|
|
50
|
+
/** Ground height between islands when no base terrain exists — deep enough to read as a fall into the void, finite so physics stays sane. */
|
|
51
|
+
export declare const ISLAND_VOID_HEIGHT = -256;
|
|
52
|
+
/**
|
|
53
|
+
* Composes a base terrain and any number of bounded islands into one world field: inside an island's
|
|
54
|
+
* rect the island's own field (sampled in island-local coordinates) wins, elsewhere the base terrain
|
|
55
|
+
* answers, and with no base the gap is `ISLAND_VOID_HEIGHT` void. Later islands win overlaps.
|
|
56
|
+
*/
|
|
57
|
+
export declare function composeIslandFields(base: TerrainField | null, islands: readonly TerrainIslandDescriptor[], voidHeight?: number): TerrainField;
|
|
58
|
+
/** The full ground field for an environment world: base `terrain` composed with any `islands`. */
|
|
59
|
+
export declare function resolveEnvironmentField(feature: EnvironmentWorldFeature): TerrainField;
|
|
60
|
+
export interface TerrainSlopeSample {
|
|
61
|
+
/** Unit XZ direction pointing downhill; `[0, 0]` on level ground. */
|
|
62
|
+
downhill: readonly [number, number];
|
|
63
|
+
/** Rise over run (tan of the slope angle); `0` on level ground. */
|
|
64
|
+
steepness: number;
|
|
65
|
+
}
|
|
66
|
+
/** Local slope from the field's normal — the input for gravity-roll, ski acceleration, and slide checks (#284.6). */
|
|
67
|
+
export declare function sampleSlope(field: TerrainField, x: number, z: number): TerrainSlopeSample;
|
|
68
|
+
/**
|
|
69
|
+
* Downhill force/acceleration from the local slope: direction × sin(slope angle) × `scale`.
|
|
70
|
+
* Add it to any integrator's XZ velocity each tick (`scale` ≈ gravity for a free-rolling body).
|
|
71
|
+
*/
|
|
72
|
+
export declare function slopeForce(field: TerrainField, x: number, z: number, scale?: number): readonly [number, number];
|
|
50
73
|
export interface TerrainPalette {
|
|
51
74
|
low: string;
|
|
52
75
|
high: string;
|
|
53
76
|
waterline: string;
|
|
54
77
|
}
|
|
55
|
-
export declare const DEFAULT_TERRAIN_MATERIAL
|
|
56
|
-
export declare const TERRAIN_MATERIAL_PALETTES: Record<
|
|
78
|
+
export declare const DEFAULT_TERRAIN_MATERIAL: TerrainMaterial;
|
|
79
|
+
export declare const TERRAIN_MATERIAL_PALETTES: Record<TerrainMaterial, TerrainPalette>;
|
|
57
80
|
export declare function resolveTerrainPalette(descriptor?: Pick<TerrainEnvironmentConfig, "material" | "colors">): TerrainPalette;
|
|
81
|
+
/**
|
|
82
|
+
* Per-position palette sampler over the descriptor's base `material`/`colors` plus its
|
|
83
|
+
* `materialRegions` — the multi-biome coloring seam. Regions paint fully inside `radius`
|
|
84
|
+
* and blend back to the surrounding palette across `falloff`; later regions win overlaps.
|
|
85
|
+
*/
|
|
86
|
+
export declare function createTerrainPaletteSampler(descriptor: Pick<TerrainEnvironmentConfig, "material" | "colors" | "materialRegions">): (x: number, z: number) => TerrainPalette;
|
|
87
|
+
export interface HeightMapFieldConfig {
|
|
88
|
+
columns: number;
|
|
89
|
+
rows: number;
|
|
90
|
+
samples: ArrayLike<number>;
|
|
91
|
+
bounds?: WorldBounds;
|
|
92
|
+
heightScale?: number;
|
|
93
|
+
baseHeight?: number;
|
|
94
|
+
waterLevel?: number;
|
|
95
|
+
}
|
|
96
|
+
export declare function heightMapField(config: HeightMapFieldConfig): TerrainField;
|
|
58
97
|
export declare function resolveTerrainField(descriptor?: TerrainEnvironmentDescriptor): TerrainField;
|
|
59
98
|
/** Returns `position` with `y` replaced by the field's ground height (plus `offset`) at its `x`/`z`. */
|
|
60
99
|
export declare function snapToGround(field: TerrainField, position: readonly [number, number, number], offset?: number): [number, number, number];
|
package/dist/world/terrain.js
CHANGED
|
@@ -150,9 +150,82 @@ export function arenaField(config = {}) {
|
|
|
150
150
|
}
|
|
151
151
|
export function groundFieldFor(world) {
|
|
152
152
|
if (world !== undefined && world.kind === "environment")
|
|
153
|
-
return
|
|
153
|
+
return resolveEnvironmentField(world);
|
|
154
154
|
return flatField();
|
|
155
155
|
}
|
|
156
|
+
/** Ground height between islands when no base terrain exists — deep enough to read as a fall into the void, finite so physics stays sane. */
|
|
157
|
+
export const ISLAND_VOID_HEIGHT = -256;
|
|
158
|
+
function islandContains(descriptor, x, z) {
|
|
159
|
+
return (Math.abs(x - descriptor.origin[0]) <= descriptor.bounds.w / 2 &&
|
|
160
|
+
Math.abs(z - descriptor.origin[1]) <= descriptor.bounds.d / 2);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Composes a base terrain and any number of bounded islands into one world field: inside an island's
|
|
164
|
+
* rect the island's own field (sampled in island-local coordinates) wins, elsewhere the base terrain
|
|
165
|
+
* answers, and with no base the gap is `ISLAND_VOID_HEIGHT` void. Later islands win overlaps.
|
|
166
|
+
*/
|
|
167
|
+
export function composeIslandFields(base, islands, voidHeight = ISLAND_VOID_HEIGHT) {
|
|
168
|
+
if (islands.length === 0)
|
|
169
|
+
return base ?? flatField();
|
|
170
|
+
const resolved = islands.map((descriptor) => ({
|
|
171
|
+
descriptor,
|
|
172
|
+
field: resolveTerrainField({ ...descriptor, kind: "terrain" }),
|
|
173
|
+
}));
|
|
174
|
+
function islandAt(x, z) {
|
|
175
|
+
for (let index = resolved.length - 1; index >= 0; index -= 1) {
|
|
176
|
+
if (islandContains(resolved[index].descriptor, x, z))
|
|
177
|
+
return resolved[index];
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const sampleHeight = (x, z) => {
|
|
182
|
+
const island = islandAt(x, z);
|
|
183
|
+
if (island !== null) {
|
|
184
|
+
return island.field.sampleHeight(x - island.descriptor.origin[0], z - island.descriptor.origin[1]);
|
|
185
|
+
}
|
|
186
|
+
return base !== null ? base.sampleHeight(x, z) : voidHeight;
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
sampleHeight,
|
|
190
|
+
sampleNormal(x, z) {
|
|
191
|
+
const island = islandAt(x, z);
|
|
192
|
+
if (island !== null) {
|
|
193
|
+
return island.field.sampleNormal(x - island.descriptor.origin[0], z - island.descriptor.origin[1]);
|
|
194
|
+
}
|
|
195
|
+
return base !== null ? base.sampleNormal(x, z) : [0, 1, 0];
|
|
196
|
+
},
|
|
197
|
+
...(base?.bounds === undefined ? {} : { bounds: base.bounds }),
|
|
198
|
+
...(base?.waterLevel === undefined ? {} : { waterLevel: base.waterLevel }),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** The full ground field for an environment world: base `terrain` composed with any `islands`. */
|
|
202
|
+
export function resolveEnvironmentField(feature) {
|
|
203
|
+
const base = feature.terrain === undefined ? null : resolveTerrainField(feature.terrain);
|
|
204
|
+
if (feature.islands === undefined || feature.islands.length === 0)
|
|
205
|
+
return base ?? flatField();
|
|
206
|
+
return composeIslandFields(base, feature.islands);
|
|
207
|
+
}
|
|
208
|
+
/** Local slope from the field's normal — the input for gravity-roll, ski acceleration, and slide checks (#284.6). */
|
|
209
|
+
export function sampleSlope(field, x, z) {
|
|
210
|
+
const [nx, ny, nz] = field.sampleNormal(x, z);
|
|
211
|
+
const horizontal = Math.hypot(nx, nz);
|
|
212
|
+
if (horizontal < 1e-9)
|
|
213
|
+
return { downhill: [0, 0], steepness: 0 };
|
|
214
|
+
return {
|
|
215
|
+
downhill: [nx / horizontal, nz / horizontal],
|
|
216
|
+
steepness: horizontal / Math.max(ny, 1e-9),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Downhill force/acceleration from the local slope: direction × sin(slope angle) × `scale`.
|
|
221
|
+
* Add it to any integrator's XZ velocity each tick (`scale` ≈ gravity for a free-rolling body).
|
|
222
|
+
*/
|
|
223
|
+
export function slopeForce(field, x, z, scale = 9.8) {
|
|
224
|
+
const [nx, , nz] = field.sampleNormal(x, z);
|
|
225
|
+
if (Math.hypot(nx, nz) < 1e-9)
|
|
226
|
+
return [0, 0];
|
|
227
|
+
return [nx * scale, nz * scale];
|
|
228
|
+
}
|
|
156
229
|
export const DEFAULT_TERRAIN_MATERIAL = "grass";
|
|
157
230
|
export const TERRAIN_MATERIAL_PALETTES = {
|
|
158
231
|
grass: { low: "#30402c", high: "#7f8b50", waterline: "#1d4c6e" },
|
|
@@ -160,9 +233,15 @@ export const TERRAIN_MATERIAL_PALETTES = {
|
|
|
160
233
|
snow: { low: "#c7d3dc", high: "#ffffff", waterline: "#3a6ea5" },
|
|
161
234
|
rock: { low: "#3a3a3d", high: "#8a8a8d", waterline: "#1d4c6e" },
|
|
162
235
|
ash: { low: "#2b2622", high: "#5c534a", waterline: "#3a3630" },
|
|
236
|
+
highland: { low: "#414f33", high: "#a3a86b", waterline: "#365f63" },
|
|
237
|
+
slate: { low: "#2f3540", high: "#7d8896", waterline: "#26404f" },
|
|
163
238
|
};
|
|
164
239
|
export function resolveTerrainPalette(descriptor = {}) {
|
|
165
|
-
const
|
|
240
|
+
const material = descriptor.material ?? DEFAULT_TERRAIN_MATERIAL;
|
|
241
|
+
const preset = TERRAIN_MATERIAL_PALETTES[material];
|
|
242
|
+
if (preset === undefined) {
|
|
243
|
+
throw new Error(`Unknown terrain material "${material}". Valid materials: ${Object.keys(TERRAIN_MATERIAL_PALETTES).join(", ")}. Use colors: { low, high, waterline } for a custom look.`);
|
|
244
|
+
}
|
|
166
245
|
const colors = descriptor.colors ?? {};
|
|
167
246
|
return {
|
|
168
247
|
low: colors.low ?? preset.low,
|
|
@@ -170,6 +249,64 @@ export function resolveTerrainPalette(descriptor = {}) {
|
|
|
170
249
|
waterline: colors.waterline ?? preset.waterline,
|
|
171
250
|
};
|
|
172
251
|
}
|
|
252
|
+
function hexToRgb(hex) {
|
|
253
|
+
const value = Number.parseInt(hex.slice(1), 16);
|
|
254
|
+
return [(value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff];
|
|
255
|
+
}
|
|
256
|
+
function rgbToHex(r, g, b) {
|
|
257
|
+
const value = ((Math.round(r) & 0xff) << 16) | ((Math.round(g) & 0xff) << 8) | (Math.round(b) & 0xff);
|
|
258
|
+
return `#${value.toString(16).padStart(6, "0")}`;
|
|
259
|
+
}
|
|
260
|
+
function mixHex(from, to, t) {
|
|
261
|
+
if (t <= 0)
|
|
262
|
+
return from;
|
|
263
|
+
if (t >= 1)
|
|
264
|
+
return to;
|
|
265
|
+
const [fr, fg, fb] = hexToRgb(from);
|
|
266
|
+
const [tr, tg, tb] = hexToRgb(to);
|
|
267
|
+
return rgbToHex(lerp(fr, tr, t), lerp(fg, tg, t), lerp(fb, tb, t));
|
|
268
|
+
}
|
|
269
|
+
function mixPalette(from, to, t) {
|
|
270
|
+
if (t <= 0)
|
|
271
|
+
return from;
|
|
272
|
+
if (t >= 1)
|
|
273
|
+
return to;
|
|
274
|
+
return {
|
|
275
|
+
low: mixHex(from.low, to.low, t),
|
|
276
|
+
high: mixHex(from.high, to.high, t),
|
|
277
|
+
waterline: mixHex(from.waterline, to.waterline, t),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Per-position palette sampler over the descriptor's base `material`/`colors` plus its
|
|
282
|
+
* `materialRegions` — the multi-biome coloring seam. Regions paint fully inside `radius`
|
|
283
|
+
* and blend back to the surrounding palette across `falloff`; later regions win overlaps.
|
|
284
|
+
*/
|
|
285
|
+
export function createTerrainPaletteSampler(descriptor) {
|
|
286
|
+
const base = resolveTerrainPalette(descriptor);
|
|
287
|
+
const regions = (descriptor.materialRegions ?? []).map((region) => ({
|
|
288
|
+
center: region.center,
|
|
289
|
+
radius: region.radius,
|
|
290
|
+
falloff: region.falloff ?? region.radius * 0.5,
|
|
291
|
+
palette: resolveTerrainPalette(region),
|
|
292
|
+
}));
|
|
293
|
+
if (regions.length === 0)
|
|
294
|
+
return () => base;
|
|
295
|
+
return (x, z) => {
|
|
296
|
+
let palette = base;
|
|
297
|
+
for (const region of regions) {
|
|
298
|
+
const distance = Math.hypot(x - region.center[0], z - region.center[1]);
|
|
299
|
+
if (distance <= region.radius) {
|
|
300
|
+
palette = region.palette;
|
|
301
|
+
}
|
|
302
|
+
else if (distance <= region.radius + region.falloff) {
|
|
303
|
+
const t = smoothstep(region.radius, region.radius + region.falloff, distance);
|
|
304
|
+
palette = mixPalette(region.palette, palette, t);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return palette;
|
|
308
|
+
};
|
|
309
|
+
}
|
|
173
310
|
function withFlattenMasks(sampleHeight, masks) {
|
|
174
311
|
const resolved = masks.map((mask) => ({
|
|
175
312
|
center: mask.center,
|
|
@@ -193,24 +330,68 @@ function withFlattenMasks(sampleHeight, masks) {
|
|
|
193
330
|
return height;
|
|
194
331
|
};
|
|
195
332
|
}
|
|
333
|
+
export function heightMapField(config) {
|
|
334
|
+
const { columns, rows, samples } = config;
|
|
335
|
+
if (columns <= 0 || rows <= 0)
|
|
336
|
+
throw new Error("heightMapField: columns/rows must be positive");
|
|
337
|
+
if (samples.length < columns * rows) {
|
|
338
|
+
throw new Error(`heightMapField: samples length ${samples.length} < columns*rows ${columns * rows}`);
|
|
339
|
+
}
|
|
340
|
+
const bounds = config.bounds ?? { w: columns, d: rows };
|
|
341
|
+
const heightScale = config.heightScale ?? 1;
|
|
342
|
+
const baseHeight = config.baseHeight ?? 0;
|
|
343
|
+
const halfW = bounds.w / 2;
|
|
344
|
+
const halfD = bounds.d / 2;
|
|
345
|
+
const sampleAt = (col, row) => {
|
|
346
|
+
const c = Math.max(0, Math.min(columns - 1, col));
|
|
347
|
+
const r = Math.max(0, Math.min(rows - 1, row));
|
|
348
|
+
return samples[r * columns + c];
|
|
349
|
+
};
|
|
350
|
+
return fieldFromHeight((x, z) => {
|
|
351
|
+
const u = columns <= 1 ? 0 : ((x + halfW) / bounds.w) * (columns - 1);
|
|
352
|
+
const v = rows <= 1 ? 0 : ((z + halfD) / bounds.d) * (rows - 1);
|
|
353
|
+
const c0 = Math.floor(u);
|
|
354
|
+
const r0 = Math.floor(v);
|
|
355
|
+
const fu = u - c0;
|
|
356
|
+
const fv = v - r0;
|
|
357
|
+
const h00 = sampleAt(c0, r0);
|
|
358
|
+
const h10 = sampleAt(c0 + 1, r0);
|
|
359
|
+
const h01 = sampleAt(c0, r0 + 1);
|
|
360
|
+
const h11 = sampleAt(c0 + 1, r0 + 1);
|
|
361
|
+
const top = lerp(h00, h10, fu);
|
|
362
|
+
const bottom = lerp(h01, h11, fu);
|
|
363
|
+
return baseHeight + heightScale * lerp(top, bottom, fv);
|
|
364
|
+
}, {
|
|
365
|
+
bounds,
|
|
366
|
+
...(config.waterLevel === undefined ? {} : { waterLevel: config.waterLevel }),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
196
369
|
export function resolveTerrainField(descriptor) {
|
|
197
370
|
if (descriptor === undefined)
|
|
198
371
|
return flatField();
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
372
|
+
if (descriptor.heightField === undefined && descriptor.heightMap !== undefined) {
|
|
373
|
+
throw new Error(`terrain heightMap "${descriptor.heightMap}" is not auto-loaded. Decode elevation samples into heightMapField({ columns, rows, samples }) and pass heightField: field.sampleHeight (or omit heightMap and set heightField directly).`);
|
|
374
|
+
}
|
|
375
|
+
const base = descriptor.heightField !== undefined
|
|
376
|
+
? fieldFromHeight(descriptor.heightField, {
|
|
377
|
+
bounds: descriptor.bounds,
|
|
378
|
+
...(descriptor.waterLevel === undefined ? {} : { waterLevel: descriptor.waterLevel }),
|
|
379
|
+
})
|
|
380
|
+
: noiseField({
|
|
381
|
+
seed: descriptor.seed,
|
|
382
|
+
amplitude: descriptor.height,
|
|
383
|
+
frequency: descriptor.frequency,
|
|
384
|
+
octaves: descriptor.octaves,
|
|
385
|
+
ridged: descriptor.ridged,
|
|
386
|
+
baseHeight: descriptor.baseHeight,
|
|
387
|
+
waterLevel: descriptor.waterLevel,
|
|
388
|
+
bounds: descriptor.bounds,
|
|
389
|
+
});
|
|
209
390
|
if (descriptor.flatten === undefined || descriptor.flatten.length === 0)
|
|
210
|
-
return
|
|
211
|
-
return fieldFromHeight(withFlattenMasks(
|
|
212
|
-
bounds:
|
|
213
|
-
waterLevel:
|
|
391
|
+
return base;
|
|
392
|
+
return fieldFromHeight(withFlattenMasks(base.sampleHeight, descriptor.flatten), {
|
|
393
|
+
bounds: base.bounds,
|
|
394
|
+
waterLevel: base.waterLevel,
|
|
214
395
|
});
|
|
215
396
|
}
|
|
216
397
|
/** Returns `position` with `y` replaced by the field's ground height (plus `offset`) at its `x`/`z`. */
|
package/dist/world/water.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface GerstnerWave {
|
|
|
9
9
|
}
|
|
10
10
|
export interface WaterSurfaceConfig {
|
|
11
11
|
level?: number;
|
|
12
|
+
/** Scheduled base level as a function of time — tides, floods (#284.1). Wins over `level`/`setLevel` whenever set. */
|
|
13
|
+
levelAt?: (time: number) => number;
|
|
12
14
|
waveHeight?: number;
|
|
13
15
|
waveScale?: number;
|
|
14
16
|
waveSpeed?: number;
|
|
@@ -16,11 +18,16 @@ export interface WaterSurfaceConfig {
|
|
|
16
18
|
choppiness?: number;
|
|
17
19
|
}
|
|
18
20
|
export interface WaterSurface {
|
|
21
|
+
/** The current base level — the initial config value until `setLevel` replaces it. With a `levelAt` schedule, use `levelAtTime` instead. */
|
|
19
22
|
readonly level: number;
|
|
20
23
|
readonly waves: readonly GerstnerWave[];
|
|
21
24
|
height(x: number, z: number, time: number): number;
|
|
22
25
|
normal(x: number, z: number, time: number): WaterNormal;
|
|
23
26
|
displace(x: number, z: number, time: number): readonly [number, number, number];
|
|
27
|
+
/** Raise or drain the water at runtime (#284.1) — every `height`/`displace` consumer (buoyancy included) sees it immediately. Ignored while a `levelAt` schedule is set. */
|
|
28
|
+
setLevel(level: number): void;
|
|
29
|
+
/** The effective base level at `time`: the `levelAt` schedule when set, else the current `level`. */
|
|
30
|
+
levelAtTime(time: number): number;
|
|
24
31
|
}
|
|
25
32
|
export declare function synthesizeWaves(config?: WaterSurfaceConfig): GerstnerWave[];
|
|
26
33
|
export declare function waterSurface(config?: WaterSurfaceConfig): WaterSurface;
|
package/dist/world/water.js
CHANGED
|
@@ -41,11 +41,13 @@ export function synthesizeWaves(config = {}) {
|
|
|
41
41
|
}
|
|
42
42
|
export function waterSurface(config = {}) {
|
|
43
43
|
const resolved = resolveConfig(config);
|
|
44
|
-
|
|
44
|
+
let currentLevel = resolved.level;
|
|
45
|
+
const levelAt = config.levelAt;
|
|
46
|
+
const levelAtTime = (time) => (levelAt !== undefined ? levelAt(time) : currentLevel);
|
|
45
47
|
const choppiness = resolved.choppiness;
|
|
46
48
|
const waves = synthesizeWaves(config);
|
|
47
49
|
const height = (x, z, time) => {
|
|
48
|
-
let y =
|
|
50
|
+
let y = levelAtTime(time);
|
|
49
51
|
for (const wave of waves) {
|
|
50
52
|
const k = TWO_PI / wave.wavelength;
|
|
51
53
|
const omega = wave.speed * k;
|
|
@@ -57,7 +59,7 @@ export function waterSurface(config = {}) {
|
|
|
57
59
|
const displace = (x, z, time) => {
|
|
58
60
|
let dx = x;
|
|
59
61
|
let dz = z;
|
|
60
|
-
let dy =
|
|
62
|
+
let dy = levelAtTime(time);
|
|
61
63
|
for (const wave of waves) {
|
|
62
64
|
const k = TWO_PI / wave.wavelength;
|
|
63
65
|
const omega = wave.speed * k;
|
|
@@ -112,11 +114,24 @@ export function waterSurface(config = {}) {
|
|
|
112
114
|
}
|
|
113
115
|
return [nx, ny, nz];
|
|
114
116
|
};
|
|
115
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
get level() {
|
|
119
|
+
return currentLevel;
|
|
120
|
+
},
|
|
121
|
+
waves,
|
|
122
|
+
height,
|
|
123
|
+
normal,
|
|
124
|
+
displace,
|
|
125
|
+
setLevel(nextLevel) {
|
|
126
|
+
currentLevel = nextLevel;
|
|
127
|
+
},
|
|
128
|
+
levelAtTime,
|
|
129
|
+
};
|
|
116
130
|
}
|
|
117
131
|
export function waterSurfaceFromDescriptor(descriptor, waves) {
|
|
118
132
|
return waterSurface({
|
|
119
133
|
level: descriptor.level,
|
|
134
|
+
...(descriptor.levelAt === undefined ? {} : { levelAt: descriptor.levelAt }),
|
|
120
135
|
waveHeight: descriptor.waveHeight,
|
|
121
136
|
waveScale: descriptor.waveScale,
|
|
122
137
|
waveSpeed: descriptor.waveSpeed,
|
package/dist/world/weather.d.ts
CHANGED
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
import type { WindVector } from "./wind.js";
|
|
2
2
|
export type WeatherKind = string;
|
|
3
3
|
export interface WeatherState {
|
|
4
|
-
/** Game-defined kind: "clear", "rain", "storm", "snow", "fog", "sandstorm". */
|
|
5
4
|
kind: WeatherKind;
|
|
6
|
-
/** 0..1 severity — scales every modifier. */
|
|
7
5
|
intensity: number;
|
|
8
6
|
wind?: WindVector;
|
|
9
7
|
}
|
|
10
|
-
/** Per-kind gameplay effect values at full intensity. Intensity interpolates from neutral. */
|
|
11
8
|
export interface WeatherModifier {
|
|
12
|
-
/** Movement traction multiplier at full intensity — <1 = mud/ice slows, 1 = normal. */
|
|
13
9
|
grip?: number;
|
|
14
|
-
/** View distance multiplier at full intensity — 0..1, 1 = clear. */
|
|
15
10
|
visibility?: number;
|
|
16
|
-
/** Structure HP lost per game-second at full intensity (hail, storm). */
|
|
17
11
|
structureDamage?: number;
|
|
18
|
-
/** Temperature delta at full intensity (a blizzard chills). */
|
|
19
12
|
chill?: number;
|
|
20
|
-
/** Lightning ignition chance per game-second at full intensity. */
|
|
21
13
|
ignition?: number;
|
|
22
|
-
/** Fire spread-rate multiplier at full intensity — rain suppresses (<1). Default neutral 1. */
|
|
23
14
|
spread?: number;
|
|
24
15
|
}
|
|
25
|
-
export type WeatherModifierTable = Record<
|
|
16
|
+
export type WeatherModifierTable<K extends string = string> = Record<K, WeatherModifier>;
|
|
17
|
+
export type WeatherKindOf<TTable extends WeatherModifierTable> = Extract<keyof TTable, string>;
|
|
26
18
|
export interface ResolvedWeather {
|
|
27
19
|
grip: number;
|
|
28
20
|
visibility: number;
|
|
@@ -31,14 +23,7 @@ export interface ResolvedWeather {
|
|
|
31
23
|
ignition: number;
|
|
32
24
|
spread: number;
|
|
33
25
|
}
|
|
34
|
-
|
|
35
|
-
* Resolve a weather state against a game-owned modifier table into concrete gameplay
|
|
36
|
-
* numbers. Multiplier-style effects (grip, visibility, spread) interpolate from the
|
|
37
|
-
* neutral 1 by intensity; rate-style effects (structureDamage, ignition, chill) scale
|
|
38
|
-
* linearly. Games read `grip`/`visibility` in movement and AI, `structureDamage` on a
|
|
39
|
-
* building tick, and `spread`/`ignition` feed the fire grid.
|
|
40
|
-
*/
|
|
41
|
-
export declare function resolveWeather(state: WeatherState, table: WeatherModifierTable): ResolvedWeather;
|
|
26
|
+
export declare function resolveWeather<TTable extends WeatherModifierTable>(state: WeatherState, table: TTable): ResolvedWeather;
|
|
42
27
|
export type FireCellState = "unburnt" | "burning" | "burnt";
|
|
43
28
|
export interface FireCell {
|
|
44
29
|
fuel: number;
|
package/dist/world/weather.js
CHANGED
|
@@ -1,25 +1,12 @@
|
|
|
1
|
-
const NEUTRAL = {
|
|
2
|
-
grip: 1,
|
|
3
|
-
visibility: 1,
|
|
4
|
-
structureDamage: 0,
|
|
5
|
-
chill: 0,
|
|
6
|
-
ignition: 0,
|
|
7
|
-
spread: 1,
|
|
8
|
-
};
|
|
9
1
|
function lerp(from, to, t) {
|
|
10
2
|
return from + (to - from) * t;
|
|
11
3
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Resolve a weather state against a game-owned modifier table into concrete gameplay
|
|
14
|
-
* numbers. Multiplier-style effects (grip, visibility, spread) interpolate from the
|
|
15
|
-
* neutral 1 by intensity; rate-style effects (structureDamage, ignition, chill) scale
|
|
16
|
-
* linearly. Games read `grip`/`visibility` in movement and AI, `structureDamage` on a
|
|
17
|
-
* building tick, and `spread`/`ignition` feed the fire grid.
|
|
18
|
-
*/
|
|
19
4
|
export function resolveWeather(state, table) {
|
|
20
5
|
const entry = table[state.kind];
|
|
21
|
-
if (entry === undefined)
|
|
22
|
-
|
|
6
|
+
if (entry === undefined) {
|
|
7
|
+
const known = Object.keys(table).sort().join(", ");
|
|
8
|
+
throw new Error(`Unknown weather kind "${state.kind}". Known kinds: ${known.length > 0 ? known : "(none)"}.`);
|
|
9
|
+
}
|
|
23
10
|
const t = Math.max(0, Math.min(1, state.intensity));
|
|
24
11
|
return {
|
|
25
12
|
grip: lerp(1, entry.grip ?? 1, t),
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type SchedulePhase } from "../time/stateSchedule.js";
|
|
2
|
+
import type { WindField, WindVector } from "./wind.js";
|
|
3
|
+
export interface WindZoneState {
|
|
4
|
+
direction: WindVector;
|
|
5
|
+
speed: number;
|
|
6
|
+
/** Announcement label for HUD countdowns ("gale", "calm", "tailwind"). */
|
|
7
|
+
label?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface WindZoneConfig {
|
|
10
|
+
id: string;
|
|
11
|
+
center: readonly [number, number];
|
|
12
|
+
radius: number;
|
|
13
|
+
/** The zone's scheduled wind states; a single phase makes a static zone. */
|
|
14
|
+
phases: readonly SchedulePhase<WindZoneState>[];
|
|
15
|
+
/** Staggers this zone's schedule against identical ones. */
|
|
16
|
+
offsetSeconds?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface WindZonesConfig {
|
|
19
|
+
zones: readonly WindZoneConfig[];
|
|
20
|
+
/** Wind outside every zone; omit for dead air. */
|
|
21
|
+
ambient?: WindField;
|
|
22
|
+
}
|
|
23
|
+
export interface WindShiftForecast {
|
|
24
|
+
zoneId: string;
|
|
25
|
+
/** Absolute time the zone's next state begins. */
|
|
26
|
+
at: number;
|
|
27
|
+
/** Seconds from the queried time until the shift. */
|
|
28
|
+
inSeconds: number;
|
|
29
|
+
next: WindZoneState;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Named, discrete wind zones over an ambient field — each zone runs a deterministic
|
|
33
|
+
* `StateSchedule` of wind states, so games get scheduled shifts with advance announcement
|
|
34
|
+
* (`forecastShift` drives the "gale in 12s" countdown) instead of one continuous field.
|
|
35
|
+
*/
|
|
36
|
+
export interface WindZones {
|
|
37
|
+
zoneIds(): readonly string[];
|
|
38
|
+
/** Innermost zone containing the point (later declarations win ties); `null` in open air. */
|
|
39
|
+
zoneAt(x: number, z: number): string | null;
|
|
40
|
+
/** The wind vector at a point and time: the containing zone's scheduled state, else ambient. */
|
|
41
|
+
windAt(x: number, z: number, t: number): WindVector;
|
|
42
|
+
/** A zone's live state; `null` for an unknown zone. */
|
|
43
|
+
stateAt(zoneId: string, t: number): WindZoneState | null;
|
|
44
|
+
/** The zone's next scheduled shift after `t`; `null` for unknown or single-phase zones. */
|
|
45
|
+
forecastShift(zoneId: string, t: number): WindShiftForecast | null;
|
|
46
|
+
}
|
|
47
|
+
export declare function createWindZones(config: WindZonesConfig): WindZones;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createStateSchedule } from "../time/stateSchedule.js";
|
|
2
|
+
function windVectorOf(state) {
|
|
3
|
+
const length = Math.hypot(state.direction[0], state.direction[1]);
|
|
4
|
+
if (length === 0 || state.speed === 0)
|
|
5
|
+
return [0, 0];
|
|
6
|
+
return [(state.direction[0] / length) * state.speed, (state.direction[1] / length) * state.speed];
|
|
7
|
+
}
|
|
8
|
+
export function createWindZones(config) {
|
|
9
|
+
const zones = new Map();
|
|
10
|
+
const order = [];
|
|
11
|
+
for (const zone of config.zones) {
|
|
12
|
+
if (zones.has(zone.id))
|
|
13
|
+
throw new Error(`duplicate wind zone: ${zone.id}`);
|
|
14
|
+
zones.set(zone.id, {
|
|
15
|
+
config: zone,
|
|
16
|
+
schedule: createStateSchedule({
|
|
17
|
+
phases: zone.phases,
|
|
18
|
+
...(zone.offsetSeconds === undefined ? {} : { offsetSeconds: zone.offsetSeconds }),
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
order.push(zone.id);
|
|
22
|
+
}
|
|
23
|
+
function zoneAt(x, z) {
|
|
24
|
+
let best = null;
|
|
25
|
+
let bestRadius = Number.POSITIVE_INFINITY;
|
|
26
|
+
for (const id of order) {
|
|
27
|
+
const zone = zones.get(id);
|
|
28
|
+
const distance = Math.hypot(x - zone.config.center[0], z - zone.config.center[1]);
|
|
29
|
+
if (distance <= zone.config.radius && zone.config.radius <= bestRadius) {
|
|
30
|
+
best = id;
|
|
31
|
+
bestRadius = zone.config.radius;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return best;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
zoneIds: () => order.slice(),
|
|
38
|
+
zoneAt,
|
|
39
|
+
windAt(x, z, t) {
|
|
40
|
+
const id = zoneAt(x, z);
|
|
41
|
+
if (id !== null)
|
|
42
|
+
return windVectorOf(zones.get(id).schedule.stateAt(t));
|
|
43
|
+
return config.ambient === undefined ? [0, 0] : config.ambient.atPoint(x, z, t);
|
|
44
|
+
},
|
|
45
|
+
stateAt(zoneId, t) {
|
|
46
|
+
const zone = zones.get(zoneId);
|
|
47
|
+
return zone === undefined ? null : zone.schedule.stateAt(t);
|
|
48
|
+
},
|
|
49
|
+
forecastShift(zoneId, t) {
|
|
50
|
+
const zone = zones.get(zoneId);
|
|
51
|
+
if (zone === undefined || zone.config.phases.length < 2)
|
|
52
|
+
return null;
|
|
53
|
+
const at = zone.schedule.nextTransitionAt(t);
|
|
54
|
+
if (!Number.isFinite(at))
|
|
55
|
+
return null;
|
|
56
|
+
return { zoneId, at, inSeconds: at - t, next: zone.schedule.stateAt(at) };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|