@jgengine/core 0.8.0 → 0.10.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 +36 -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.d.ts +13 -0
- package/dist/ai/spawnDirector.js +41 -17
- package/dist/ai/spawnPoint.d.ts +5 -0
- package/dist/ai/spawnPoint.js +5 -0
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/audio/audioFalloff.d.ts +5 -2
- package/dist/audio/music.d.ts +53 -0
- package/dist/audio/music.js +34 -0
- package/dist/audio/synth.d.ts +60 -0
- package/dist/audio/synth.js +8 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +14 -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/dotField.d.ts +43 -0
- package/dist/combat/dotField.js +48 -0
- package/dist/combat/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +214 -129
- package/dist/combat/regenShield.d.ts +35 -0
- package/dist/combat/regenShield.js +45 -0
- 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 +52 -0
- package/dist/combat/shotOrigin.js +86 -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/editor/commands.d.ts +65 -0
- package/dist/editor/commands.js +193 -0
- package/dist/editor/document.d.ts +40 -0
- package/dist/editor/document.js +153 -0
- package/dist/editor/index.d.ts +30 -0
- package/dist/editor/index.js +6 -0
- package/dist/editor/types.d.ts +69 -0
- package/dist/editor/types.js +29 -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/connectedPlayers.d.ts +28 -0
- package/dist/game/connectedPlayers.js +21 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +44 -1
- package/dist/game/defineGame.js +1 -0
- package/dist/game/events.d.ts +25 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/keyValueStore.d.ts +25 -0
- package/dist/game/keyValueStore.js +61 -0
- package/dist/game/levelSequence.d.ts +24 -2
- package/dist/game/levelSequence.js +51 -2
- package/dist/game/lootTable.d.ts +14 -1
- package/dist/game/lootTable.js +28 -7
- package/dist/game/modelAnimation.d.ts +8 -0
- package/dist/game/modelAnimation.js +18 -0
- package/dist/game/playableGame.d.ts +116 -4
- 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 +27 -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 +8 -0
- package/dist/index.js +8 -0
- package/dist/input/actionBindings.d.ts +2 -0
- package/dist/input/actionBindings.js +1 -0
- package/dist/input/axisInput.d.ts +33 -3
- package/dist/input/axisInput.js +60 -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/interaction/contextMenu.d.ts +1 -0
- package/dist/interaction/contextMenu.js +1 -0
- package/dist/interaction/proximityPrompt.d.ts +3 -1
- package/dist/interaction/proximityPrompt.js +3 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +44 -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/movementModel.d.ts +4 -0
- package/dist/movement/movementModel.js +13 -0
- package/dist/movement/playerMovement.d.ts +41 -0
- package/dist/movement/playerMovement.js +181 -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/rng.d.ts +2 -0
- package/dist/random/rng.js +2 -0
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/render/postProcessing.d.ts +56 -0
- package/dist/render/postProcessing.js +1 -0
- package/dist/runtime/adapter.d.ts +19 -0
- package/dist/runtime/adapter.js +11 -5
- 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 +72 -8
- package/dist/runtime/gameContext.js +374 -61
- 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/hostedGameRunner.d.ts +49 -0
- package/dist/runtime/hostedGameRunner.js +62 -0
- package/dist/runtime/hostedWorldSession.d.ts +65 -0
- package/dist/runtime/hostedWorldSession.js +57 -0
- package/dist/runtime/inputSnapshot.d.ts +18 -0
- package/dist/runtime/inputSnapshot.js +7 -0
- package/dist/runtime/motionIntents.d.ts +16 -6
- package/dist/runtime/motionIntents.js +31 -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/runtime/worldChannel.d.ts +58 -0
- package/dist/runtime/worldChannel.js +69 -0
- package/dist/runtime/worldMirror.d.ts +24 -0
- package/dist/runtime/worldMirror.js +31 -0
- package/dist/runtime/worldReplication.d.ts +46 -0
- package/dist/runtime/worldReplication.js +185 -0
- package/dist/runtime/worldSnapshot.d.ts +17 -0
- package/dist/runtime/worldSnapshot.js +15 -0
- package/dist/scene/assetCatalog.d.ts +1 -0
- package/dist/scene/colliders.d.ts +50 -0
- package/dist/scene/colliders.js +101 -0
- package/dist/scene/entityStats.d.ts +4 -0
- package/dist/scene/entityStats.js +21 -0
- package/dist/scene/entityStore.d.ts +51 -5
- package/dist/scene/entityStore.js +119 -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.d.ts +4 -0
- package/dist/store/observableKeyedStore.js +17 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/tactics/fallingGrid.d.ts +2 -2
- package/dist/tactics/fallingGrid.js +2 -2
- 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/gameLayout.d.ts +125 -0
- package/dist/ui/gameLayout.js +129 -0
- 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/ui/orientation.d.ts +39 -0
- package/dist/ui/orientation.js +49 -0
- package/dist/ui/swingTimer.d.ts +42 -0
- package/dist/ui/swingTimer.js +38 -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 +212 -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 +21 -4
- package/dist/world/environmentSummary.js +22 -3
- package/dist/world/features.d.ts +185 -8
- package/dist/world/features.js +64 -6
- 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/roads.d.ts +47 -0
- package/dist/world/roads.js +146 -0
- package/dist/world/streets.d.ts +80 -0
- package/dist/world/streets.js +144 -0
- package/dist/world/terrain.d.ts +69 -3
- package/dist/world/terrain.js +309 -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 +3013 -1726
- package/package.json +12 -2
package/dist/world/terrain.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nearestOnPath } from "./roads.js";
|
|
1
2
|
function clamp01(value) {
|
|
2
3
|
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
3
4
|
}
|
|
@@ -13,6 +14,7 @@ function hash2(ix, iz, seed) {
|
|
|
13
14
|
h ^= h >>> 12;
|
|
14
15
|
return (h >>> 0) / 4294967295;
|
|
15
16
|
}
|
|
17
|
+
/** Smoothly interpolated 2D value noise in `[-1, 1]` for the given seed. */
|
|
16
18
|
export function valueNoise(x, z, seed) {
|
|
17
19
|
const x0 = Math.floor(x);
|
|
18
20
|
const z0 = Math.floor(z);
|
|
@@ -28,6 +30,7 @@ export function valueNoise(x, z, seed) {
|
|
|
28
30
|
const bottom = lerp(v01, v11, sx);
|
|
29
31
|
return lerp(top, bottom, sz) * 2 - 1;
|
|
30
32
|
}
|
|
33
|
+
/** Layers `valueNoise` octaves per `config` into a single normalized noise sample. */
|
|
31
34
|
export function fractalNoise(x, z, config) {
|
|
32
35
|
let amplitude = 1;
|
|
33
36
|
let frequency = config.frequency;
|
|
@@ -57,6 +60,7 @@ export function seedFrom(value, fallback) {
|
|
|
57
60
|
}
|
|
58
61
|
return fallback;
|
|
59
62
|
}
|
|
63
|
+
/** Derives a `TerrainField.sampleNormal` from a height sampler via finite-difference gradients. */
|
|
60
64
|
export function withNormal(sampleHeight) {
|
|
61
65
|
const epsilon = 0.75;
|
|
62
66
|
return (x, z) => {
|
|
@@ -81,9 +85,11 @@ export const FLAT_FIELD = {
|
|
|
81
85
|
sampleHeight: () => 0,
|
|
82
86
|
sampleNormal: () => [0, 1, 0],
|
|
83
87
|
};
|
|
88
|
+
/** A flat, zero-height `TerrainField` for arenas with no elevation. */
|
|
84
89
|
export function flatField() {
|
|
85
90
|
return FLAT_FIELD;
|
|
86
91
|
}
|
|
92
|
+
/** Builds a `TerrainField` whose height is fractal noise shaped by `config`. */
|
|
87
93
|
export function noiseField(config = {}) {
|
|
88
94
|
const fractal = {
|
|
89
95
|
seed: seedFrom(config.seed, 1337),
|
|
@@ -120,6 +126,7 @@ function smoothstep(edge0, edge1, value) {
|
|
|
120
126
|
const t = clamp01((value - edge0) / (edge1 - edge0));
|
|
121
127
|
return t * t * (3 - 2 * t);
|
|
122
128
|
}
|
|
129
|
+
/** Builds a `TerrainField` with a flat spawn plateau, rolling hills, and a basin, for combat arenas. */
|
|
123
130
|
export function arenaField(config = {}) {
|
|
124
131
|
const seed = seedFrom(config.seed ?? "arena", 7);
|
|
125
132
|
const rolling = {
|
|
@@ -150,9 +157,86 @@ export function arenaField(config = {}) {
|
|
|
150
157
|
}
|
|
151
158
|
export function groundFieldFor(world) {
|
|
152
159
|
if (world !== undefined && world.kind === "environment")
|
|
153
|
-
return
|
|
160
|
+
return resolveEnvironmentField(world);
|
|
154
161
|
return flatField();
|
|
155
162
|
}
|
|
163
|
+
/** Whether a world declares real terrain (base heightfield or islands) rather than a flat plane — gates terrain-floor sampling in the movement controllers. */
|
|
164
|
+
export function hasEnvironmentTerrain(world) {
|
|
165
|
+
return world?.kind === "environment" && (world.terrain !== undefined || (world.islands?.length ?? 0) > 0);
|
|
166
|
+
}
|
|
167
|
+
/** Ground height between islands when no base terrain exists — deep enough to read as a fall into the void, finite so physics stays sane. */
|
|
168
|
+
export const ISLAND_VOID_HEIGHT = -256;
|
|
169
|
+
function islandContains(descriptor, x, z) {
|
|
170
|
+
return (Math.abs(x - descriptor.origin[0]) <= descriptor.bounds.w / 2 &&
|
|
171
|
+
Math.abs(z - descriptor.origin[1]) <= descriptor.bounds.d / 2);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Composes a base terrain and any number of bounded islands into one world field: inside an island's
|
|
175
|
+
* rect the island's own field (sampled in island-local coordinates) wins, elsewhere the base terrain
|
|
176
|
+
* answers, and with no base the gap is `ISLAND_VOID_HEIGHT` void. Later islands win overlaps.
|
|
177
|
+
*/
|
|
178
|
+
export function composeIslandFields(base, islands, voidHeight = ISLAND_VOID_HEIGHT) {
|
|
179
|
+
if (islands.length === 0)
|
|
180
|
+
return base ?? flatField();
|
|
181
|
+
const resolved = islands.map((descriptor) => ({
|
|
182
|
+
descriptor,
|
|
183
|
+
field: resolveTerrainField({ ...descriptor, kind: "terrain" }),
|
|
184
|
+
}));
|
|
185
|
+
function islandAt(x, z) {
|
|
186
|
+
for (let index = resolved.length - 1; index >= 0; index -= 1) {
|
|
187
|
+
if (islandContains(resolved[index].descriptor, x, z))
|
|
188
|
+
return resolved[index];
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const sampleHeight = (x, z) => {
|
|
193
|
+
const island = islandAt(x, z);
|
|
194
|
+
if (island !== null) {
|
|
195
|
+
return island.field.sampleHeight(x - island.descriptor.origin[0], z - island.descriptor.origin[1]);
|
|
196
|
+
}
|
|
197
|
+
return base !== null ? base.sampleHeight(x, z) : voidHeight;
|
|
198
|
+
};
|
|
199
|
+
return {
|
|
200
|
+
sampleHeight,
|
|
201
|
+
sampleNormal(x, z) {
|
|
202
|
+
const island = islandAt(x, z);
|
|
203
|
+
if (island !== null) {
|
|
204
|
+
return island.field.sampleNormal(x - island.descriptor.origin[0], z - island.descriptor.origin[1]);
|
|
205
|
+
}
|
|
206
|
+
return base !== null ? base.sampleNormal(x, z) : [0, 1, 0];
|
|
207
|
+
},
|
|
208
|
+
...(base?.bounds === undefined ? {} : { bounds: base.bounds }),
|
|
209
|
+
...(base?.waterLevel === undefined ? {} : { waterLevel: base.waterLevel }),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/** The full ground field for an environment world: base `terrain` composed with any `islands`. */
|
|
213
|
+
export function resolveEnvironmentField(feature) {
|
|
214
|
+
const base = feature.terrain === undefined ? null : resolveTerrainField(feature.terrain);
|
|
215
|
+
if (feature.islands === undefined || feature.islands.length === 0)
|
|
216
|
+
return base ?? flatField();
|
|
217
|
+
return composeIslandFields(base, feature.islands);
|
|
218
|
+
}
|
|
219
|
+
/** Local slope from the field's normal — the input for gravity-roll, ski acceleration, and slide checks (#284.6). */
|
|
220
|
+
export function sampleSlope(field, x, z) {
|
|
221
|
+
const [nx, ny, nz] = field.sampleNormal(x, z);
|
|
222
|
+
const horizontal = Math.hypot(nx, nz);
|
|
223
|
+
if (horizontal < 1e-9)
|
|
224
|
+
return { downhill: [0, 0], steepness: 0 };
|
|
225
|
+
return {
|
|
226
|
+
downhill: [nx / horizontal, nz / horizontal],
|
|
227
|
+
steepness: horizontal / Math.max(ny, 1e-9),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Downhill force/acceleration from the local slope: direction × sin(slope angle) × `scale`.
|
|
232
|
+
* Add it to any integrator's XZ velocity each tick (`scale` ≈ gravity for a free-rolling body).
|
|
233
|
+
*/
|
|
234
|
+
export function slopeForce(field, x, z, scale = 9.8) {
|
|
235
|
+
const [nx, , nz] = field.sampleNormal(x, z);
|
|
236
|
+
if (Math.hypot(nx, nz) < 1e-9)
|
|
237
|
+
return [0, 0];
|
|
238
|
+
return [nx * scale, nz * scale];
|
|
239
|
+
}
|
|
156
240
|
export const DEFAULT_TERRAIN_MATERIAL = "grass";
|
|
157
241
|
export const TERRAIN_MATERIAL_PALETTES = {
|
|
158
242
|
grass: { low: "#30402c", high: "#7f8b50", waterline: "#1d4c6e" },
|
|
@@ -160,9 +244,41 @@ export const TERRAIN_MATERIAL_PALETTES = {
|
|
|
160
244
|
snow: { low: "#c7d3dc", high: "#ffffff", waterline: "#3a6ea5" },
|
|
161
245
|
rock: { low: "#3a3a3d", high: "#8a8a8d", waterline: "#1d4c6e" },
|
|
162
246
|
ash: { low: "#2b2622", high: "#5c534a", waterline: "#3a3630" },
|
|
247
|
+
highland: { low: "#414f33", high: "#a3a86b", waterline: "#365f63" },
|
|
248
|
+
slate: { low: "#2f3540", high: "#7d8896", waterline: "#26404f" },
|
|
249
|
+
};
|
|
250
|
+
const DEFAULT_TERRAIN_DETAIL = {
|
|
251
|
+
rockColor: "#6f7175",
|
|
252
|
+
sandColor: "#c2b283",
|
|
253
|
+
snowColor: "#eef3f7",
|
|
254
|
+
rockSlopeStart: 0.42,
|
|
255
|
+
snowHeight: 26,
|
|
256
|
+
detailScale: 5,
|
|
257
|
+
macroScale: 45,
|
|
258
|
+
roughness: 0.9,
|
|
259
|
+
strength: 1,
|
|
163
260
|
};
|
|
261
|
+
/** Fill a `TerrainDetailConfig` with defaults; `waterLevel` falls back to the terrain's own water level. */
|
|
262
|
+
export function resolveTerrainDetail(config, terrainWaterLevel = 0) {
|
|
263
|
+
return {
|
|
264
|
+
rockColor: config.rockColor ?? DEFAULT_TERRAIN_DETAIL.rockColor,
|
|
265
|
+
sandColor: config.sandColor ?? DEFAULT_TERRAIN_DETAIL.sandColor,
|
|
266
|
+
snowColor: config.snowColor ?? DEFAULT_TERRAIN_DETAIL.snowColor,
|
|
267
|
+
rockSlopeStart: config.rockSlopeStart ?? DEFAULT_TERRAIN_DETAIL.rockSlopeStart,
|
|
268
|
+
snowHeight: config.snowHeight ?? DEFAULT_TERRAIN_DETAIL.snowHeight,
|
|
269
|
+
detailScale: config.detailScale ?? DEFAULT_TERRAIN_DETAIL.detailScale,
|
|
270
|
+
macroScale: config.macroScale ?? DEFAULT_TERRAIN_DETAIL.macroScale,
|
|
271
|
+
roughness: config.roughness ?? DEFAULT_TERRAIN_DETAIL.roughness,
|
|
272
|
+
strength: config.strength ?? DEFAULT_TERRAIN_DETAIL.strength,
|
|
273
|
+
waterLevel: config.waterLevel ?? terrainWaterLevel,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
164
276
|
export function resolveTerrainPalette(descriptor = {}) {
|
|
165
|
-
const
|
|
277
|
+
const material = descriptor.material ?? DEFAULT_TERRAIN_MATERIAL;
|
|
278
|
+
const preset = TERRAIN_MATERIAL_PALETTES[material];
|
|
279
|
+
if (preset === undefined) {
|
|
280
|
+
throw new Error(`Unknown terrain material "${material}". Valid materials: ${Object.keys(TERRAIN_MATERIAL_PALETTES).join(", ")}. Use colors: { low, high, waterline } for a custom look.`);
|
|
281
|
+
}
|
|
166
282
|
const colors = descriptor.colors ?? {};
|
|
167
283
|
return {
|
|
168
284
|
low: colors.low ?? preset.low,
|
|
@@ -170,6 +286,137 @@ export function resolveTerrainPalette(descriptor = {}) {
|
|
|
170
286
|
waterline: colors.waterline ?? preset.waterline,
|
|
171
287
|
};
|
|
172
288
|
}
|
|
289
|
+
function hexToRgb(hex) {
|
|
290
|
+
const value = Number.parseInt(hex.slice(1), 16);
|
|
291
|
+
return [(value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff];
|
|
292
|
+
}
|
|
293
|
+
function rgbToHex(r, g, b) {
|
|
294
|
+
const value = ((Math.round(r) & 0xff) << 16) | ((Math.round(g) & 0xff) << 8) | (Math.round(b) & 0xff);
|
|
295
|
+
return `#${value.toString(16).padStart(6, "0")}`;
|
|
296
|
+
}
|
|
297
|
+
function mixHex(from, to, t) {
|
|
298
|
+
if (t <= 0)
|
|
299
|
+
return from;
|
|
300
|
+
if (t >= 1)
|
|
301
|
+
return to;
|
|
302
|
+
const [fr, fg, fb] = hexToRgb(from);
|
|
303
|
+
const [tr, tg, tb] = hexToRgb(to);
|
|
304
|
+
return rgbToHex(lerp(fr, tr, t), lerp(fg, tg, t), lerp(fb, tb, t));
|
|
305
|
+
}
|
|
306
|
+
function mixPalette(from, to, t) {
|
|
307
|
+
if (t <= 0)
|
|
308
|
+
return from;
|
|
309
|
+
if (t >= 1)
|
|
310
|
+
return to;
|
|
311
|
+
return {
|
|
312
|
+
low: mixHex(from.low, to.low, t),
|
|
313
|
+
high: mixHex(from.high, to.high, t),
|
|
314
|
+
waterline: mixHex(from.waterline, to.waterline, t),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Per-z palette sampler over a descriptor's `biomeBands` — ordered zones that cross-fade their
|
|
319
|
+
* ground palette across a `fade`-wide window centered on the midpoint z between adjacent centers.
|
|
320
|
+
* Below the first / above the last band clamps to that band's palette. Returns `fallback` when no
|
|
321
|
+
* bands are declared. Pure math, unit-testable independent of rendering.
|
|
322
|
+
*/
|
|
323
|
+
export function createBiomeBandSampler(bands, fallback) {
|
|
324
|
+
if (bands.length === 0)
|
|
325
|
+
return () => fallback;
|
|
326
|
+
const resolved = bands
|
|
327
|
+
.map((band) => ({ z: band.z, fade: band.fade ?? 64, palette: resolveTerrainPalette(band) }))
|
|
328
|
+
.sort((a, b) => a.z - b.z);
|
|
329
|
+
const first = resolved[0];
|
|
330
|
+
const last = resolved[resolved.length - 1];
|
|
331
|
+
return (z) => {
|
|
332
|
+
if (z <= first.z)
|
|
333
|
+
return first.palette;
|
|
334
|
+
if (z >= last.z)
|
|
335
|
+
return last.palette;
|
|
336
|
+
for (let i = 0; i < resolved.length - 1; i += 1) {
|
|
337
|
+
const lower = resolved[i];
|
|
338
|
+
const upper = resolved[i + 1];
|
|
339
|
+
if (z < lower.z || z > upper.z)
|
|
340
|
+
continue;
|
|
341
|
+
const boundary = (lower.z + upper.z) / 2;
|
|
342
|
+
const half = Math.min(lower.fade, upper.fade) / 2;
|
|
343
|
+
if (z <= boundary - half)
|
|
344
|
+
return lower.palette;
|
|
345
|
+
if (z >= boundary + half)
|
|
346
|
+
return upper.palette;
|
|
347
|
+
return mixPalette(lower.palette, upper.palette, smoothstep(boundary - half, boundary + half, z));
|
|
348
|
+
}
|
|
349
|
+
return fallback;
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Per-position palette sampler over the descriptor's base `material`/`colors` plus its z-ordered
|
|
354
|
+
* `biomeBands` (painted first) and radial `materialRegions` (painted over) — the multi-biome coloring
|
|
355
|
+
* seam. Regions paint fully inside `radius` and blend back across `falloff`; later regions win overlaps.
|
|
356
|
+
*/
|
|
357
|
+
export function createTerrainPaletteSampler(descriptor) {
|
|
358
|
+
const base = resolveTerrainPalette(descriptor);
|
|
359
|
+
const bandSampler = descriptor.biomeBands === undefined || descriptor.biomeBands.length === 0
|
|
360
|
+
? null
|
|
361
|
+
: createBiomeBandSampler(descriptor.biomeBands, base);
|
|
362
|
+
const regions = (descriptor.materialRegions ?? []).map((region) => ({
|
|
363
|
+
...resolveRegionShape(region),
|
|
364
|
+
palette: resolveTerrainPalette(region),
|
|
365
|
+
}));
|
|
366
|
+
if (regions.length === 0 && bandSampler === null)
|
|
367
|
+
return () => base;
|
|
368
|
+
return (x, z) => {
|
|
369
|
+
let palette = bandSampler === null ? base : bandSampler(z);
|
|
370
|
+
for (const region of regions) {
|
|
371
|
+
const distance = region.coreDistance(x, z);
|
|
372
|
+
if (distance <= 0) {
|
|
373
|
+
palette = region.palette;
|
|
374
|
+
}
|
|
375
|
+
else if (distance <= region.falloff) {
|
|
376
|
+
const t = smoothstep(0, region.falloff, distance);
|
|
377
|
+
palette = mixPalette(region.palette, palette, t);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return palette;
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Resolves a `TerrainMaterialRegion` of any shape to a `coreDistance(x, z)` — 0 inside the painted
|
|
385
|
+
* core, growing positive outward — plus its blend-ring `falloff`, so the palette sampler blends every
|
|
386
|
+
* shape through one shape-agnostic loop.
|
|
387
|
+
*/
|
|
388
|
+
function resolveRegionShape(region) {
|
|
389
|
+
if (region.shape === "polyline") {
|
|
390
|
+
const half = region.width * 0.5;
|
|
391
|
+
return {
|
|
392
|
+
falloff: region.falloff ?? half,
|
|
393
|
+
coreDistance: (x, z) => {
|
|
394
|
+
const sample = nearestOnPath(region.points, x, z);
|
|
395
|
+
return sample === null ? Number.POSITIVE_INFINITY : Math.max(0, sample.distance - half);
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (region.shape === "rect") {
|
|
400
|
+
const [hx, hz] = region.halfExtents;
|
|
401
|
+
const angle = region.rotationY ?? 0;
|
|
402
|
+
const cos = Math.cos(angle);
|
|
403
|
+
const sin = Math.sin(angle);
|
|
404
|
+
return {
|
|
405
|
+
falloff: region.falloff ?? Math.min(hx, hz) * 0.5,
|
|
406
|
+
coreDistance: (x, z) => {
|
|
407
|
+
const dx = x - region.center[0];
|
|
408
|
+
const dz = z - region.center[1];
|
|
409
|
+
const lx = dx * cos + dz * sin;
|
|
410
|
+
const lz = -dx * sin + dz * cos;
|
|
411
|
+
return Math.hypot(Math.max(0, Math.abs(lx) - hx), Math.max(0, Math.abs(lz) - hz));
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
falloff: region.falloff ?? region.radius * 0.5,
|
|
417
|
+
coreDistance: (x, z) => Math.max(0, Math.hypot(x - region.center[0], z - region.center[1]) - region.radius),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
173
420
|
function withFlattenMasks(sampleHeight, masks) {
|
|
174
421
|
const resolved = masks.map((mask) => ({
|
|
175
422
|
center: mask.center,
|
|
@@ -193,24 +440,69 @@ function withFlattenMasks(sampleHeight, masks) {
|
|
|
193
440
|
return height;
|
|
194
441
|
};
|
|
195
442
|
}
|
|
443
|
+
export function heightMapField(config) {
|
|
444
|
+
const { columns, rows, samples } = config;
|
|
445
|
+
if (columns <= 0 || rows <= 0)
|
|
446
|
+
throw new Error("heightMapField: columns/rows must be positive");
|
|
447
|
+
if (samples.length < columns * rows) {
|
|
448
|
+
throw new Error(`heightMapField: samples length ${samples.length} < columns*rows ${columns * rows}`);
|
|
449
|
+
}
|
|
450
|
+
const bounds = config.bounds ?? { w: columns, d: rows };
|
|
451
|
+
const heightScale = config.heightScale ?? 1;
|
|
452
|
+
const baseHeight = config.baseHeight ?? 0;
|
|
453
|
+
const halfW = bounds.w / 2;
|
|
454
|
+
const halfD = bounds.d / 2;
|
|
455
|
+
const sampleAt = (col, row) => {
|
|
456
|
+
const c = Math.max(0, Math.min(columns - 1, col));
|
|
457
|
+
const r = Math.max(0, Math.min(rows - 1, row));
|
|
458
|
+
return samples[r * columns + c];
|
|
459
|
+
};
|
|
460
|
+
return fieldFromHeight((x, z) => {
|
|
461
|
+
const u = columns <= 1 ? 0 : ((x + halfW) / bounds.w) * (columns - 1);
|
|
462
|
+
const v = rows <= 1 ? 0 : ((z + halfD) / bounds.d) * (rows - 1);
|
|
463
|
+
const c0 = Math.floor(u);
|
|
464
|
+
const r0 = Math.floor(v);
|
|
465
|
+
const fu = u - c0;
|
|
466
|
+
const fv = v - r0;
|
|
467
|
+
const h00 = sampleAt(c0, r0);
|
|
468
|
+
const h10 = sampleAt(c0 + 1, r0);
|
|
469
|
+
const h01 = sampleAt(c0, r0 + 1);
|
|
470
|
+
const h11 = sampleAt(c0 + 1, r0 + 1);
|
|
471
|
+
const top = lerp(h00, h10, fu);
|
|
472
|
+
const bottom = lerp(h01, h11, fu);
|
|
473
|
+
return baseHeight + heightScale * lerp(top, bottom, fv);
|
|
474
|
+
}, {
|
|
475
|
+
bounds,
|
|
476
|
+
...(config.waterLevel === undefined ? {} : { waterLevel: config.waterLevel }),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/** Resolves a `TerrainEnvironmentDescriptor` into a concrete `TerrainField`, applying flatten masks. */
|
|
196
480
|
export function resolveTerrainField(descriptor) {
|
|
197
481
|
if (descriptor === undefined)
|
|
198
482
|
return flatField();
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
483
|
+
if (descriptor.heightField === undefined && descriptor.heightMap !== undefined) {
|
|
484
|
+
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).`);
|
|
485
|
+
}
|
|
486
|
+
const base = descriptor.heightField !== undefined
|
|
487
|
+
? fieldFromHeight(descriptor.heightField, {
|
|
488
|
+
bounds: descriptor.bounds,
|
|
489
|
+
...(descriptor.waterLevel === undefined ? {} : { waterLevel: descriptor.waterLevel }),
|
|
490
|
+
})
|
|
491
|
+
: noiseField({
|
|
492
|
+
seed: descriptor.seed,
|
|
493
|
+
amplitude: descriptor.height,
|
|
494
|
+
frequency: descriptor.frequency,
|
|
495
|
+
octaves: descriptor.octaves,
|
|
496
|
+
ridged: descriptor.ridged,
|
|
497
|
+
baseHeight: descriptor.baseHeight,
|
|
498
|
+
waterLevel: descriptor.waterLevel,
|
|
499
|
+
bounds: descriptor.bounds,
|
|
500
|
+
});
|
|
209
501
|
if (descriptor.flatten === undefined || descriptor.flatten.length === 0)
|
|
210
|
-
return
|
|
211
|
-
return fieldFromHeight(withFlattenMasks(
|
|
212
|
-
bounds:
|
|
213
|
-
waterLevel:
|
|
502
|
+
return base;
|
|
503
|
+
return fieldFromHeight(withFlattenMasks(base.sampleHeight, descriptor.flatten), {
|
|
504
|
+
bounds: base.bounds,
|
|
505
|
+
waterLevel: base.waterLevel,
|
|
214
506
|
});
|
|
215
507
|
}
|
|
216
508
|
/** Returns `position` with `y` replaced by the field's ground height (plus `offset`) at its `x`/`z`. */
|
|
@@ -226,6 +518,7 @@ export function snapEntityToGround(entities, id, field, offset = 0) {
|
|
|
226
518
|
return entities.setPose(id, { position: snapToGround(field, entity.position, offset) });
|
|
227
519
|
}
|
|
228
520
|
export const DEFAULT_MAX_WALK_SLOPE = 0.6;
|
|
521
|
+
/** Zeroes out a movement step's x or z component where it would climb steeper than `maxSlope`. */
|
|
229
522
|
export function resolveGroundStep(field, x, z, stepX, stepZ, maxSlope = DEFAULT_MAX_WALK_SLOPE) {
|
|
230
523
|
const baseHeight = field.sampleHeight(x, z);
|
|
231
524
|
const tooSteep = (dx, dz) => {
|
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
|
+
}
|