@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.
Files changed (228) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/ai/flock.d.ts +36 -0
  3. package/dist/ai/flock.js +182 -0
  4. package/dist/ai/groupAssist.d.ts +19 -0
  5. package/dist/ai/groupAssist.js +59 -0
  6. package/dist/ai/laneSelect.d.ts +21 -0
  7. package/dist/ai/laneSelect.js +37 -0
  8. package/dist/ai/mobBrain.d.ts +68 -0
  9. package/dist/ai/mobBrain.js +115 -0
  10. package/dist/ai/spawnDirector.js +1 -1
  11. package/dist/ai/threat.d.ts +2 -0
  12. package/dist/ai/threat.js +34 -0
  13. package/dist/cartridge/runtime.d.ts +15 -0
  14. package/dist/cartridge/runtime.js +556 -0
  15. package/dist/cartridge/spec.d.ts +188 -0
  16. package/dist/cartridge/spec.js +19 -0
  17. package/dist/cartridge/validate.d.ts +2 -0
  18. package/dist/cartridge/validate.js +78 -0
  19. package/dist/combat/abilityKit.d.ts +17 -2
  20. package/dist/combat/abilityKit.js +62 -13
  21. package/dist/combat/castRunner.d.ts +40 -0
  22. package/dist/combat/castRunner.js +56 -0
  23. package/dist/combat/comboPoints.d.ts +14 -0
  24. package/dist/combat/comboPoints.js +52 -0
  25. package/dist/combat/defensiveWindow.d.ts +1 -1
  26. package/dist/combat/defensiveWindow.js +5 -3
  27. package/dist/combat/projectiles.d.ts +33 -12
  28. package/dist/combat/projectiles.js +201 -129
  29. package/dist/combat/resistance.d.ts +16 -4
  30. package/dist/combat/resistance.js +43 -7
  31. package/dist/combat/resourcePool.d.ts +20 -0
  32. package/dist/combat/resourcePool.js +55 -0
  33. package/dist/combat/shotOrigin.d.ts +40 -0
  34. package/dist/combat/shotOrigin.js +57 -0
  35. package/dist/combat/telegraph.d.ts +25 -0
  36. package/dist/combat/telegraph.js +47 -0
  37. package/dist/crafting/recipe.js +4 -1
  38. package/dist/devtools/devtools.d.ts +72 -24
  39. package/dist/devtools/devtools.js +449 -101
  40. package/dist/devtools/transformTunables.js +3 -3
  41. package/dist/devtools/tunableSchema.d.ts +140 -0
  42. package/dist/devtools/tunableSchema.js +476 -0
  43. package/dist/faction/factions.d.ts +33 -0
  44. package/dist/faction/factions.js +76 -0
  45. package/dist/faction/reputation.d.ts +32 -0
  46. package/dist/faction/reputation.js +82 -0
  47. package/dist/game/chat.d.ts +2 -0
  48. package/dist/game/chat.js +9 -1
  49. package/dist/game/chatFilter.d.ts +15 -0
  50. package/dist/game/chatFilter.js +55 -0
  51. package/dist/game/controlGate.d.ts +4 -0
  52. package/dist/game/controlGate.js +7 -0
  53. package/dist/game/defineGame.d.ts +2 -0
  54. package/dist/game/events.d.ts +10 -0
  55. package/dist/game/gamePhase.d.ts +20 -0
  56. package/dist/game/gamePhase.js +21 -0
  57. package/dist/game/lootTable.d.ts +10 -1
  58. package/dist/game/lootTable.js +27 -7
  59. package/dist/game/playableGame.d.ts +50 -1
  60. package/dist/game/playableGame.js +1 -1
  61. package/dist/game/quest.d.ts +7 -3
  62. package/dist/game/quest.js +17 -8
  63. package/dist/game/race.d.ts +39 -1
  64. package/dist/game/race.js +96 -11
  65. package/dist/game/recordBook.d.ts +29 -0
  66. package/dist/game/recordBook.js +86 -0
  67. package/dist/game/talents.d.ts +47 -0
  68. package/dist/game/talents.js +140 -0
  69. package/dist/index.d.ts +1 -0
  70. package/dist/index.js +1 -0
  71. package/dist/input/axisInput.d.ts +26 -3
  72. package/dist/input/axisInput.js +44 -4
  73. package/dist/input/bindingOverrides.d.ts +25 -0
  74. package/dist/input/bindingOverrides.js +95 -0
  75. package/dist/input/pointerAxis.d.ts +30 -0
  76. package/dist/input/pointerAxis.js +31 -0
  77. package/dist/input/touchScheme.d.ts +6 -0
  78. package/dist/input/touchScheme.js +45 -7
  79. package/dist/meta/changelog.d.ts +1 -1
  80. package/dist/meta/changelog.js +23 -1
  81. package/dist/movement/dash.d.ts +3 -0
  82. package/dist/movement/dash.js +19 -5
  83. package/dist/movement/glideModel.d.ts +44 -0
  84. package/dist/movement/glideModel.js +54 -0
  85. package/dist/movement/grappleSwing.d.ts +34 -0
  86. package/dist/movement/grappleSwing.js +56 -0
  87. package/dist/movement/leaderTrail.d.ts +29 -0
  88. package/dist/movement/leaderTrail.js +67 -0
  89. package/dist/movement/steering.d.ts +27 -0
  90. package/dist/movement/steering.js +40 -0
  91. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  92. package/dist/multiplayer/feedWriteGate.js +12 -0
  93. package/dist/multiplayer/presenceModel.d.ts +14 -2
  94. package/dist/multiplayer/presenceModel.js +15 -3
  95. package/dist/nav/corridors.d.ts +30 -0
  96. package/dist/nav/corridors.js +69 -0
  97. package/dist/nav/navGrid.js +30 -4
  98. package/dist/nav/railGraph.d.ts +66 -0
  99. package/dist/nav/railGraph.js +129 -0
  100. package/dist/nav/timetable.d.ts +41 -0
  101. package/dist/nav/timetable.js +94 -0
  102. package/dist/physics/buoyancy.d.ts +6 -0
  103. package/dist/physics/buoyancy.js +14 -0
  104. package/dist/physics/flowTube.d.ts +32 -0
  105. package/dist/physics/flowTube.js +64 -0
  106. package/dist/physics/forceVolume.d.ts +27 -0
  107. package/dist/physics/forceVolume.js +38 -0
  108. package/dist/physics/kinematicVehicle.d.ts +52 -0
  109. package/dist/physics/kinematicVehicle.js +81 -0
  110. package/dist/physics/physicsWorld.d.ts +13 -6
  111. package/dist/physics/physicsWorld.js +169 -40
  112. package/dist/physics/ragdoll.js +1 -1
  113. package/dist/physics/vehicleBody.js +2 -1
  114. package/dist/random/seedLink.d.ts +5 -0
  115. package/dist/random/seedLink.js +35 -0
  116. package/dist/runtime/cameraDirector.d.ts +6 -1
  117. package/dist/runtime/cameraDirector.js +6 -0
  118. package/dist/runtime/commandRunner.d.ts +2 -2
  119. package/dist/runtime/commandRunner.js +2 -2
  120. package/dist/runtime/gameContext.d.ts +21 -0
  121. package/dist/runtime/gameContext.js +184 -9
  122. package/dist/runtime/gameRuntime.d.ts +5 -1
  123. package/dist/runtime/gameRuntime.js +13 -12
  124. package/dist/runtime/hostPersistence.d.ts +9 -1
  125. package/dist/runtime/hostPersistence.js +23 -8
  126. package/dist/runtime/inputSnapshot.d.ts +5 -0
  127. package/dist/runtime/inputSnapshot.js +5 -0
  128. package/dist/runtime/motionIntents.d.ts +14 -6
  129. package/dist/runtime/motionIntents.js +20 -2
  130. package/dist/runtime/snapshot.d.ts +1 -0
  131. package/dist/runtime/snapshot.js +1 -1
  132. package/dist/runtime/visibility.d.ts +74 -0
  133. package/dist/runtime/visibility.js +127 -0
  134. package/dist/scene/colliders.d.ts +44 -0
  135. package/dist/scene/colliders.js +77 -0
  136. package/dist/scene/entityStore.d.ts +22 -5
  137. package/dist/scene/entityStore.js +59 -12
  138. package/dist/scene/objectQuery.d.ts +13 -2
  139. package/dist/scene/objectQuery.js +34 -2
  140. package/dist/scene/objectStore.d.ts +2 -0
  141. package/dist/scene/objectStore.js +31 -12
  142. package/dist/scene/sceneRaycast.d.ts +80 -0
  143. package/dist/scene/sceneRaycast.js +284 -0
  144. package/dist/scene/spatial.d.ts +11 -16
  145. package/dist/scene/spatial.js +99 -38
  146. package/dist/sensor/recordingBuffer.d.ts +6 -0
  147. package/dist/sensor/recordingBuffer.js +7 -0
  148. package/dist/sensor/replayLoop.d.ts +39 -0
  149. package/dist/sensor/replayLoop.js +64 -0
  150. package/dist/sensor/visionCone.d.ts +31 -0
  151. package/dist/sensor/visionCone.js +72 -0
  152. package/dist/session/extraction.d.ts +2 -2
  153. package/dist/session/extraction.js +13 -2
  154. package/dist/settings/settingsModel.d.ts +97 -0
  155. package/dist/settings/settingsModel.js +100 -0
  156. package/dist/stats/statModifiers.d.ts +5 -2
  157. package/dist/stats/statModifiers.js +32 -14
  158. package/dist/store/observableKeyedStore.js +8 -2
  159. package/dist/survival/decayMeter.d.ts +1 -1
  160. package/dist/survival/decayMeter.js +0 -1
  161. package/dist/time/beatClock.d.ts +18 -0
  162. package/dist/time/beatClock.js +24 -0
  163. package/dist/time/idleProgress.d.ts +29 -0
  164. package/dist/time/idleProgress.js +31 -0
  165. package/dist/time/serverTick.d.ts +7 -1
  166. package/dist/time/serverTick.js +27 -4
  167. package/dist/time/stateSchedule.d.ts +57 -0
  168. package/dist/time/stateSchedule.js +121 -0
  169. package/dist/turn/commit.d.ts +2 -2
  170. package/dist/turn/commit.js +2 -2
  171. package/dist/ui/hudLayout.d.ts +58 -0
  172. package/dist/ui/hudLayout.js +207 -0
  173. package/dist/ui/hudScale.d.ts +44 -0
  174. package/dist/ui/hudScale.js +55 -0
  175. package/dist/visibility/assetStreaming.d.ts +66 -0
  176. package/dist/visibility/assetStreaming.js +201 -0
  177. package/dist/visibility/bounds.d.ts +66 -0
  178. package/dist/visibility/bounds.js +113 -0
  179. package/dist/visibility/camera.d.ts +21 -0
  180. package/dist/visibility/camera.js +3 -0
  181. package/dist/visibility/config.d.ts +21 -0
  182. package/dist/visibility/config.js +1 -0
  183. package/dist/visibility/diagnostics.d.ts +26 -0
  184. package/dist/visibility/diagnostics.js +36 -0
  185. package/dist/visibility/distance.d.ts +11 -0
  186. package/dist/visibility/distance.js +30 -0
  187. package/dist/visibility/frustum.d.ts +47 -0
  188. package/dist/visibility/frustum.js +186 -0
  189. package/dist/visibility/occlusion.d.ts +36 -0
  190. package/dist/visibility/occlusion.js +45 -0
  191. package/dist/visibility/settings.d.ts +75 -0
  192. package/dist/visibility/settings.js +49 -0
  193. package/dist/visibility/simulationCulling.d.ts +34 -0
  194. package/dist/visibility/simulationCulling.js +36 -0
  195. package/dist/visibility/spatialIndex.d.ts +45 -0
  196. package/dist/visibility/spatialIndex.js +198 -0
  197. package/dist/visibility/visibilitySystem.d.ts +90 -0
  198. package/dist/visibility/visibilitySystem.js +257 -0
  199. package/dist/world/buildings.d.ts +6 -0
  200. package/dist/world/buildings.js +152 -0
  201. package/dist/world/carve.d.ts +7 -2
  202. package/dist/world/carve.js +17 -7
  203. package/dist/world/cellStates.d.ts +45 -0
  204. package/dist/world/cellStates.js +103 -0
  205. package/dist/world/environmentSummary.d.ts +10 -2
  206. package/dist/world/environmentSummary.js +11 -2
  207. package/dist/world/features.d.ts +56 -8
  208. package/dist/world/features.js +24 -3
  209. package/dist/world/gridInstances.d.ts +7 -8
  210. package/dist/world/gridInstances.js +145 -7
  211. package/dist/world/lod.d.ts +17 -0
  212. package/dist/world/lod.js +66 -0
  213. package/dist/world/mapLayers.d.ts +60 -0
  214. package/dist/world/mapLayers.js +44 -0
  215. package/dist/world/massing.d.ts +45 -0
  216. package/dist/world/massing.js +581 -0
  217. package/dist/world/minimap.d.ts +13 -1
  218. package/dist/world/minimap.js +21 -2
  219. package/dist/world/terrain.d.ts +42 -3
  220. package/dist/world/terrain.js +197 -16
  221. package/dist/world/water.d.ts +7 -0
  222. package/dist/world/water.js +19 -4
  223. package/dist/world/weather.d.ts +3 -18
  224. package/dist/world/weather.js +4 -17
  225. package/dist/world/windZones.d.ts +47 -0
  226. package/dist/world/windZones.js +59 -0
  227. package/llms.txt +2585 -1704
  228. package/package.json +12 -2
@@ -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 = "grass";
56
- export declare const TERRAIN_MATERIAL_PALETTES: Record<string, TerrainPalette>;
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];
@@ -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 resolveTerrainField(world.terrain);
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 preset = TERRAIN_MATERIAL_PALETTES[descriptor.material ?? DEFAULT_TERRAIN_MATERIAL] ?? TERRAIN_MATERIAL_PALETTES[DEFAULT_TERRAIN_MATERIAL];
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
- const noise = noiseField({
200
- seed: descriptor.seed,
201
- amplitude: descriptor.height,
202
- frequency: descriptor.frequency,
203
- octaves: descriptor.octaves,
204
- ridged: descriptor.ridged,
205
- baseHeight: descriptor.baseHeight,
206
- waterLevel: descriptor.waterLevel,
207
- bounds: descriptor.bounds,
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 noise;
211
- return fieldFromHeight(withFlattenMasks(noise.sampleHeight, descriptor.flatten), {
212
- bounds: noise.bounds,
213
- waterLevel: noise.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`. */
@@ -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;
@@ -41,11 +41,13 @@ export function synthesizeWaves(config = {}) {
41
41
  }
42
42
  export function waterSurface(config = {}) {
43
43
  const resolved = resolveConfig(config);
44
- const level = resolved.level;
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 = level;
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 = level;
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 { level, waves, height, normal, displace };
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,
@@ -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<WeatherKind, WeatherModifier>;
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;
@@ -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
- return { ...NEUTRAL };
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
+ }