@jgengine/core 0.7.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 (329) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +2 -0
  3. package/dist/ai/flock.d.ts +36 -0
  4. package/dist/ai/flock.js +182 -0
  5. package/dist/ai/groupAssist.d.ts +19 -0
  6. package/dist/ai/groupAssist.js +59 -0
  7. package/dist/ai/laneSelect.d.ts +21 -0
  8. package/dist/ai/laneSelect.js +37 -0
  9. package/dist/ai/mobBrain.d.ts +68 -0
  10. package/dist/ai/mobBrain.js +115 -0
  11. package/dist/ai/spawnDirector.d.ts +5 -0
  12. package/dist/ai/spawnDirector.js +14 -2
  13. package/dist/ai/threat.d.ts +2 -0
  14. package/dist/ai/threat.js +34 -0
  15. package/dist/anim/easing.d.ts +14 -0
  16. package/dist/anim/easing.js +55 -0
  17. package/dist/behaviour/behaviour.d.ts +136 -0
  18. package/dist/behaviour/behaviour.js +388 -0
  19. package/dist/cards/cardPile.d.ts +1 -0
  20. package/dist/cards/cardPile.js +12 -3
  21. package/dist/cartridge/runtime.d.ts +15 -0
  22. package/dist/cartridge/runtime.js +556 -0
  23. package/dist/cartridge/spec.d.ts +188 -0
  24. package/dist/cartridge/spec.js +19 -0
  25. package/dist/cartridge/validate.d.ts +2 -0
  26. package/dist/cartridge/validate.js +78 -0
  27. package/dist/combat/abilityKit.d.ts +22 -2
  28. package/dist/combat/abilityKit.js +72 -13
  29. package/dist/combat/castRunner.d.ts +40 -0
  30. package/dist/combat/castRunner.js +56 -0
  31. package/dist/combat/comboPoints.d.ts +14 -0
  32. package/dist/combat/comboPoints.js +52 -0
  33. package/dist/combat/defensiveWindow.d.ts +1 -1
  34. package/dist/combat/defensiveWindow.js +5 -3
  35. package/dist/combat/effects.d.ts +1 -1
  36. package/dist/combat/effects.js +9 -6
  37. package/dist/combat/projectiles.d.ts +64 -6
  38. package/dist/combat/projectiles.js +232 -74
  39. package/dist/combat/resistance.d.ts +16 -4
  40. package/dist/combat/resistance.js +43 -7
  41. package/dist/combat/resourcePool.d.ts +20 -0
  42. package/dist/combat/resourcePool.js +55 -0
  43. package/dist/combat/shotOrigin.d.ts +40 -0
  44. package/dist/combat/shotOrigin.js +57 -0
  45. package/dist/combat/telegraph.d.ts +30 -1
  46. package/dist/combat/telegraph.js +61 -0
  47. package/dist/commands/commandRegistry.d.ts +1 -1
  48. package/dist/commands/commandRegistry.js +2 -1
  49. package/dist/crafting/recipe.js +4 -1
  50. package/dist/data/dataSource.d.ts +26 -0
  51. package/dist/data/dataSource.js +93 -0
  52. package/dist/data/devProxy.d.ts +11 -0
  53. package/dist/data/devProxy.js +38 -0
  54. package/dist/data/fetchJson.d.ts +20 -0
  55. package/dist/data/fetchJson.js +43 -0
  56. package/dist/data/jsonDataSource.d.ts +4 -0
  57. package/dist/data/jsonDataSource.js +6 -0
  58. package/dist/devtools/devtools.d.ts +205 -0
  59. package/dist/devtools/devtools.js +832 -0
  60. package/dist/devtools/transformTunables.d.ts +14 -0
  61. package/dist/devtools/transformTunables.js +38 -0
  62. package/dist/devtools/tunableSchema.d.ts +140 -0
  63. package/dist/devtools/tunableSchema.js +476 -0
  64. package/dist/faction/factions.d.ts +33 -0
  65. package/dist/faction/factions.js +76 -0
  66. package/dist/faction/reputation.d.ts +32 -0
  67. package/dist/faction/reputation.js +82 -0
  68. package/dist/game/chat.d.ts +68 -0
  69. package/dist/game/chat.js +173 -0
  70. package/dist/game/chatFilter.d.ts +15 -0
  71. package/dist/game/chatFilter.js +55 -0
  72. package/dist/game/controlGate.d.ts +4 -0
  73. package/dist/game/controlGate.js +7 -0
  74. package/dist/game/defineGame.d.ts +10 -2
  75. package/dist/game/defineGame.js +2 -1
  76. package/dist/game/events.d.ts +35 -0
  77. package/dist/game/gamePhase.d.ts +20 -0
  78. package/dist/game/gamePhase.js +21 -0
  79. package/dist/game/levelSequence.d.ts +44 -0
  80. package/dist/game/levelSequence.js +78 -0
  81. package/dist/game/lootTable.d.ts +10 -1
  82. package/dist/game/lootTable.js +27 -7
  83. package/dist/game/playableGame.d.ts +286 -6
  84. package/dist/game/playableGame.js +6 -1
  85. package/dist/game/progression.d.ts +1 -0
  86. package/dist/game/progression.js +23 -2
  87. package/dist/game/quest.d.ts +7 -3
  88. package/dist/game/quest.js +17 -8
  89. package/dist/game/race.d.ts +44 -1
  90. package/dist/game/race.js +116 -11
  91. package/dist/game/recordBook.d.ts +29 -0
  92. package/dist/game/recordBook.js +86 -0
  93. package/dist/game/social.d.ts +42 -0
  94. package/dist/game/social.js +122 -0
  95. package/dist/game/spawnPoints.d.ts +19 -0
  96. package/dist/game/spawnPoints.js +23 -0
  97. package/dist/game/talents.d.ts +47 -0
  98. package/dist/game/talents.js +140 -0
  99. package/dist/game/worldItem.d.ts +3 -0
  100. package/dist/game/worldItem.js +12 -6
  101. package/dist/index.d.ts +1 -0
  102. package/dist/index.js +1 -0
  103. package/dist/input/actionBindings.d.ts +10 -0
  104. package/dist/input/actionBindings.js +14 -0
  105. package/dist/input/axisInput.d.ts +26 -3
  106. package/dist/input/axisInput.js +44 -4
  107. package/dist/input/bindingOverrides.d.ts +25 -0
  108. package/dist/input/bindingOverrides.js +95 -0
  109. package/dist/input/gestureSurface.d.ts +31 -0
  110. package/dist/input/gestureSurface.js +120 -0
  111. package/dist/input/pointer.d.ts +45 -0
  112. package/dist/input/pointer.js +48 -0
  113. package/dist/input/pointerAxis.d.ts +30 -0
  114. package/dist/input/pointerAxis.js +31 -0
  115. package/dist/input/touchScheme.d.ts +89 -0
  116. package/dist/input/touchScheme.js +153 -0
  117. package/dist/item/affix.d.ts +1 -1
  118. package/dist/item/affix.js +1 -19
  119. package/dist/meta/changelog.d.ts +1 -1
  120. package/dist/meta/changelog.js +59 -1
  121. package/dist/movement/dash.d.ts +3 -0
  122. package/dist/movement/dash.js +19 -5
  123. package/dist/movement/glideModel.d.ts +44 -0
  124. package/dist/movement/glideModel.js +54 -0
  125. package/dist/movement/grappleSwing.d.ts +34 -0
  126. package/dist/movement/grappleSwing.js +56 -0
  127. package/dist/movement/leaderTrail.d.ts +29 -0
  128. package/dist/movement/leaderTrail.js +67 -0
  129. package/dist/movement/movementModel.d.ts +36 -3
  130. package/dist/movement/movementModel.js +81 -5
  131. package/dist/movement/steering.d.ts +27 -0
  132. package/dist/movement/steering.js +40 -0
  133. package/dist/movement/voxelController.d.ts +61 -0
  134. package/dist/movement/voxelController.js +229 -0
  135. package/dist/multiplayer/chatContract.d.ts +42 -0
  136. package/dist/multiplayer/chatContract.js +57 -0
  137. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  138. package/dist/multiplayer/feedWriteGate.js +12 -0
  139. package/dist/multiplayer/identity.d.ts +13 -0
  140. package/dist/multiplayer/identity.js +20 -0
  141. package/dist/multiplayer/poseSyncGate.d.ts +1 -0
  142. package/dist/multiplayer/poseSyncGate.js +13 -1
  143. package/dist/multiplayer/presenceModel.d.ts +17 -2
  144. package/dist/multiplayer/presenceModel.js +30 -4
  145. package/dist/multiplayer/voiceContract.d.ts +44 -0
  146. package/dist/multiplayer/voiceContract.js +108 -0
  147. package/dist/nav/corridors.d.ts +30 -0
  148. package/dist/nav/corridors.js +69 -0
  149. package/dist/nav/navConstrain.d.ts +14 -0
  150. package/dist/nav/navConstrain.js +26 -0
  151. package/dist/nav/navFromEnvironment.d.ts +11 -0
  152. package/dist/nav/navFromEnvironment.js +16 -0
  153. package/dist/nav/navGrid.d.ts +10 -0
  154. package/dist/nav/navGrid.js +48 -5
  155. package/dist/nav/pathFollow.d.ts +4 -2
  156. package/dist/nav/pathFollow.js +5 -3
  157. package/dist/nav/railGraph.d.ts +66 -0
  158. package/dist/nav/railGraph.js +129 -0
  159. package/dist/nav/timetable.d.ts +41 -0
  160. package/dist/nav/timetable.js +94 -0
  161. package/dist/physics/ballisticSweep.d.ts +18 -0
  162. package/dist/physics/ballisticSweep.js +49 -0
  163. package/dist/physics/buoyancy.d.ts +6 -0
  164. package/dist/physics/buoyancy.js +14 -0
  165. package/dist/physics/flowTube.d.ts +32 -0
  166. package/dist/physics/flowTube.js +64 -0
  167. package/dist/physics/forceVolume.d.ts +27 -0
  168. package/dist/physics/forceVolume.js +43 -5
  169. package/dist/physics/kinematicVehicle.d.ts +52 -0
  170. package/dist/physics/kinematicVehicle.js +81 -0
  171. package/dist/physics/physicsWorld.d.ts +54 -8
  172. package/dist/physics/physicsWorld.js +400 -79
  173. package/dist/physics/ragdoll.js +1 -1
  174. package/dist/physics/spatialGrid.d.ts +1 -1
  175. package/dist/physics/spatialGrid.js +5 -0
  176. package/dist/physics/vehicleBody.d.ts +6 -0
  177. package/dist/physics/vehicleBody.js +18 -18
  178. package/dist/puzzle/cellGrid.d.ts +26 -0
  179. package/dist/puzzle/cellGrid.js +110 -0
  180. package/dist/puzzle/fallingPiece.d.ts +24 -0
  181. package/dist/puzzle/fallingPiece.js +51 -0
  182. package/dist/random/nameGen.d.ts +18 -0
  183. package/dist/random/nameGen.js +35 -0
  184. package/dist/random/rng.d.ts +2 -0
  185. package/dist/random/rng.js +23 -0
  186. package/dist/random/seedLink.d.ts +5 -0
  187. package/dist/random/seedLink.js +35 -0
  188. package/dist/runtime/adapter.d.ts +35 -0
  189. package/dist/runtime/adapter.js +37 -1
  190. package/dist/runtime/cameraDirector.d.ts +15 -0
  191. package/dist/runtime/cameraDirector.js +31 -0
  192. package/dist/runtime/commandRunner.d.ts +2 -2
  193. package/dist/runtime/commandRunner.js +2 -2
  194. package/dist/runtime/gameContext.d.ts +65 -2
  195. package/dist/runtime/gameContext.js +280 -9
  196. package/dist/runtime/gameRuntime.d.ts +5 -1
  197. package/dist/runtime/gameRuntime.js +13 -12
  198. package/dist/runtime/hostPersistence.d.ts +30 -1
  199. package/dist/runtime/hostPersistence.js +70 -5
  200. package/dist/runtime/inputSnapshot.d.ts +12 -0
  201. package/dist/runtime/inputSnapshot.js +17 -0
  202. package/dist/runtime/motionIntents.d.ts +33 -0
  203. package/dist/runtime/motionIntents.js +43 -0
  204. package/dist/runtime/snapshot.d.ts +1 -0
  205. package/dist/runtime/snapshot.js +1 -1
  206. package/dist/runtime/transport.d.ts +32 -0
  207. package/dist/runtime/visibility.d.ts +74 -0
  208. package/dist/runtime/visibility.js +127 -0
  209. package/dist/scene/colliders.d.ts +44 -0
  210. package/dist/scene/colliders.js +77 -0
  211. package/dist/scene/entityStore.d.ts +30 -5
  212. package/dist/scene/entityStore.js +83 -13
  213. package/dist/scene/movementSpeed.d.ts +35 -0
  214. package/dist/scene/movementSpeed.js +20 -0
  215. package/dist/scene/objectQuery.d.ts +29 -0
  216. package/dist/scene/objectQuery.js +140 -0
  217. package/dist/scene/objectStore.d.ts +13 -0
  218. package/dist/scene/objectStore.js +130 -6
  219. package/dist/scene/paintLayer.d.ts +15 -0
  220. package/dist/scene/paintLayer.js +53 -0
  221. package/dist/scene/sceneRaycast.d.ts +80 -0
  222. package/dist/scene/sceneRaycast.js +284 -0
  223. package/dist/scene/spatial.d.ts +17 -1
  224. package/dist/scene/spatial.js +140 -17
  225. package/dist/sensor/concealment.d.ts +21 -0
  226. package/dist/sensor/concealment.js +63 -0
  227. package/dist/sensor/freezeMonitor.d.ts +17 -0
  228. package/dist/sensor/freezeMonitor.js +35 -0
  229. package/dist/sensor/recordingBuffer.d.ts +6 -0
  230. package/dist/sensor/recordingBuffer.js +7 -0
  231. package/dist/sensor/replayLoop.d.ts +39 -0
  232. package/dist/sensor/replayLoop.js +64 -0
  233. package/dist/sensor/visionCone.d.ts +31 -0
  234. package/dist/sensor/visionCone.js +72 -0
  235. package/dist/session/extraction.d.ts +2 -2
  236. package/dist/session/extraction.js +13 -2
  237. package/dist/session/roles.d.ts +6 -0
  238. package/dist/session/roles.js +32 -0
  239. package/dist/session/roundState.d.ts +41 -17
  240. package/dist/session/roundState.js +67 -38
  241. package/dist/settings/settingsModel.d.ts +97 -0
  242. package/dist/settings/settingsModel.js +100 -0
  243. package/dist/stats/statModifiers.d.ts +5 -2
  244. package/dist/stats/statModifiers.js +32 -14
  245. package/dist/store/observableKeyedStore.js +8 -2
  246. package/dist/survival/decayMeter.d.ts +1 -1
  247. package/dist/survival/decayMeter.js +0 -1
  248. package/dist/tactics/fallingGrid.d.ts +62 -0
  249. package/dist/tactics/fallingGrid.js +158 -0
  250. package/dist/tactics/tacticalGrid.d.ts +11 -0
  251. package/dist/tactics/tacticalGrid.js +17 -0
  252. package/dist/time/beatClock.d.ts +18 -0
  253. package/dist/time/beatClock.js +24 -0
  254. package/dist/time/idleProgress.d.ts +29 -0
  255. package/dist/time/idleProgress.js +31 -0
  256. package/dist/time/serverTick.d.ts +7 -1
  257. package/dist/time/serverTick.js +27 -4
  258. package/dist/time/simClock.d.ts +25 -1
  259. package/dist/time/simClock.js +28 -3
  260. package/dist/time/stateSchedule.d.ts +57 -0
  261. package/dist/time/stateSchedule.js +121 -0
  262. package/dist/turn/commit.d.ts +2 -2
  263. package/dist/turn/commit.js +2 -2
  264. package/dist/turn/intent.d.ts +14 -0
  265. package/dist/turn/intent.js +29 -0
  266. package/dist/turn/turnLoop.d.ts +2 -0
  267. package/dist/turn/turnLoop.js +8 -1
  268. package/dist/ui/hudLayout.d.ts +58 -0
  269. package/dist/ui/hudLayout.js +207 -0
  270. package/dist/ui/hudScale.d.ts +44 -0
  271. package/dist/ui/hudScale.js +55 -0
  272. package/dist/visibility/assetStreaming.d.ts +66 -0
  273. package/dist/visibility/assetStreaming.js +201 -0
  274. package/dist/visibility/bounds.d.ts +66 -0
  275. package/dist/visibility/bounds.js +113 -0
  276. package/dist/visibility/camera.d.ts +21 -0
  277. package/dist/visibility/camera.js +3 -0
  278. package/dist/visibility/config.d.ts +21 -0
  279. package/dist/visibility/config.js +1 -0
  280. package/dist/visibility/diagnostics.d.ts +26 -0
  281. package/dist/visibility/diagnostics.js +36 -0
  282. package/dist/visibility/distance.d.ts +11 -0
  283. package/dist/visibility/distance.js +30 -0
  284. package/dist/visibility/frustum.d.ts +47 -0
  285. package/dist/visibility/frustum.js +186 -0
  286. package/dist/visibility/occlusion.d.ts +36 -0
  287. package/dist/visibility/occlusion.js +45 -0
  288. package/dist/visibility/settings.d.ts +75 -0
  289. package/dist/visibility/settings.js +49 -0
  290. package/dist/visibility/simulationCulling.d.ts +34 -0
  291. package/dist/visibility/simulationCulling.js +36 -0
  292. package/dist/visibility/spatialIndex.d.ts +45 -0
  293. package/dist/visibility/spatialIndex.js +198 -0
  294. package/dist/visibility/visibilitySystem.d.ts +90 -0
  295. package/dist/visibility/visibilitySystem.js +257 -0
  296. package/dist/world/buildings.d.ts +6 -0
  297. package/dist/world/buildings.js +152 -0
  298. package/dist/world/carve.d.ts +7 -2
  299. package/dist/world/carve.js +17 -7
  300. package/dist/world/cellStates.d.ts +45 -0
  301. package/dist/world/cellStates.js +103 -0
  302. package/dist/world/environmentSummary.d.ts +61 -0
  303. package/dist/world/environmentSummary.js +138 -0
  304. package/dist/world/features.d.ts +138 -9
  305. package/dist/world/features.js +73 -2
  306. package/dist/world/gridInstances.d.ts +12 -0
  307. package/dist/world/gridInstances.js +160 -0
  308. package/dist/world/lod.d.ts +17 -0
  309. package/dist/world/lod.js +66 -0
  310. package/dist/world/mapLayers.d.ts +60 -0
  311. package/dist/world/mapLayers.js +44 -0
  312. package/dist/world/massing.d.ts +45 -0
  313. package/dist/world/massing.js +581 -0
  314. package/dist/world/minimap.d.ts +13 -1
  315. package/dist/world/minimap.js +21 -2
  316. package/dist/world/placementController.d.ts +22 -1
  317. package/dist/world/placementController.js +56 -0
  318. package/dist/world/terrain.d.ts +62 -1
  319. package/dist/world/terrain.js +253 -9
  320. package/dist/world/voxelField.d.ts +54 -0
  321. package/dist/world/voxelField.js +241 -0
  322. package/dist/world/water.d.ts +7 -0
  323. package/dist/world/water.js +19 -4
  324. package/dist/world/weather.d.ts +3 -18
  325. package/dist/world/weather.js +4 -17
  326. package/dist/world/windZones.d.ts +47 -0
  327. package/dist/world/windZones.js +59 -0
  328. package/llms.txt +4983 -0
  329. package/package.json +14 -3
@@ -6,12 +6,24 @@ export interface PlacementHit {
6
6
  point: PlacementVec3;
7
7
  normal: PlacementVec3;
8
8
  }
9
+ /** A fixed buildable slot the placement controller snaps to instead of grid/free positioning. */
10
+ export interface PlacementSlot {
11
+ id: string;
12
+ center: PlacementVec3;
13
+ /** Capture radius around `center`. Defaults to `config.slotRadius`, then `1.5`. */
14
+ radius?: number;
15
+ /** `false` hides the slot from hover resolution without releasing it. Default `true`. */
16
+ enabled?: boolean;
17
+ }
9
18
  export interface PlacementControllerConfig {
10
19
  footprint: Footprint;
11
20
  rules?: PlacementRules;
12
21
  snapMode?: SnapMode;
13
22
  grid?: number;
14
23
  quarterTurns?: number;
24
+ slots?: readonly PlacementSlot[];
25
+ /** Fallback capture radius for slots that don't set their own. Default `1.5`. */
26
+ slotRadius?: number;
15
27
  }
16
28
  export interface PlacementPreview {
17
29
  center: Vec2;
@@ -20,9 +32,11 @@ export interface PlacementPreview {
20
32
  footprint: Footprint;
21
33
  aabb: Aabb;
22
34
  valid: boolean;
23
- reason?: "out-of-bounds" | "overlap";
35
+ reason?: "out-of-bounds" | "overlap" | "no-slot";
24
36
  snapMode: SnapMode;
25
37
  normal: PlacementVec3;
38
+ /** Id of the resolved slot, when the controller is in slot mode and a slot matched. */
39
+ slotId?: string;
26
40
  }
27
41
  export interface PlacementCommit {
28
42
  center: Vec2;
@@ -31,6 +45,8 @@ export interface PlacementCommit {
31
45
  quarterTurns: number;
32
46
  footprint: Footprint;
33
47
  aabb: Aabb;
48
+ /** Id of the slot this commit occupied, when the controller is in slot mode. */
49
+ slotId?: string;
34
50
  }
35
51
  export interface PlacementController {
36
52
  hover(hit: PlacementHit): PlacementPreview;
@@ -41,6 +57,11 @@ export interface PlacementController {
41
57
  setFootprint(footprint: Footprint): void;
42
58
  setRules(rules: PlacementRules): void;
43
59
  setGrid(grid: number): void;
60
+ setSlots(slots: readonly PlacementSlot[]): void;
61
+ /** Id of the currently hovered slot, or `null` when idle, out of slot mode, or unmatched. */
62
+ hoveredSlotId(): string | null;
63
+ /** Frees a previously committed slot so it can match again. */
64
+ releaseSlot(id: string): void;
44
65
  commit(): PlacementCommit | null;
45
66
  reset(): void;
46
67
  }
@@ -1,6 +1,10 @@
1
1
  import { footprintAabb, snapToGrid } from "./geometry.js";
2
2
  import { validatePlacement } from "./placement.js";
3
3
  const SNAP_ORDER = ["grid", "free", "surface"];
4
+ const DEFAULT_SLOT_RADIUS = 1.5;
5
+ function distanceVec3(a, b) {
6
+ return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
7
+ }
4
8
  export function quarterTurnsToRotationY(quarterTurns) {
5
9
  const turns = ((quarterTurns % 4) + 4) % 4;
6
10
  return turns === 0 ? 0 : -turns * (Math.PI / 2);
@@ -11,6 +15,9 @@ export function createPlacementController(config) {
11
15
  let snapMode = config.snapMode ?? "grid";
12
16
  let grid = config.grid ?? rules.snap ?? 1;
13
17
  let quarterTurns = ((config.quarterTurns ?? 0) % 4 + 4) % 4;
18
+ let slots = config.slots;
19
+ const slotRadius = config.slotRadius ?? DEFAULT_SLOT_RADIUS;
20
+ const occupiedSlots = new Set();
14
21
  let lastHit = null;
15
22
  let preview = null;
16
23
  function snapCenter(raw) {
@@ -18,7 +25,40 @@ export function createPlacementController(config) {
18
25
  return snapToGrid(raw, grid);
19
26
  return raw;
20
27
  }
28
+ function nearestSlot(hit) {
29
+ let best = null;
30
+ let bestDistance = Number.POSITIVE_INFINITY;
31
+ for (const slot of slots ?? []) {
32
+ if (slot.enabled === false || occupiedSlots.has(slot.id))
33
+ continue;
34
+ const distance = distanceVec3(hit.point, slot.center);
35
+ if (distance <= (slot.radius ?? slotRadius) && distance < bestDistance) {
36
+ best = slot;
37
+ bestDistance = distance;
38
+ }
39
+ }
40
+ return best;
41
+ }
42
+ function computeSlotPreview(hit) {
43
+ const slot = nearestSlot(hit);
44
+ const center = slot === null ? [hit.point[0], hit.point[2]] : [slot.center[0], slot.center[2]];
45
+ const next = {
46
+ center,
47
+ y: slot === null ? hit.point[1] : slot.center[1],
48
+ quarterTurns,
49
+ footprint,
50
+ aabb: footprintAabb(center, footprint, quarterTurns),
51
+ valid: slot !== null,
52
+ snapMode,
53
+ normal: hit.normal,
54
+ ...(slot === null ? { reason: "no-slot" } : { slotId: slot.id }),
55
+ };
56
+ preview = next;
57
+ return next;
58
+ }
21
59
  function compute(hit) {
60
+ if (slots !== undefined)
61
+ return computeSlotPreview(hit);
22
62
  const raw = [hit.point[0], hit.point[2]];
23
63
  const center = snapCenter(raw);
24
64
  const rulesWithoutSnap = snapMode === "grid" ? { ...rules, snap: undefined } : rules;
@@ -76,9 +116,24 @@ export function createPlacementController(config) {
76
116
  if (lastHit !== null)
77
117
  compute(lastHit);
78
118
  },
119
+ setSlots(next) {
120
+ slots = next;
121
+ if (lastHit !== null)
122
+ compute(lastHit);
123
+ },
124
+ hoveredSlotId() {
125
+ return preview?.slotId ?? null;
126
+ },
127
+ releaseSlot(id) {
128
+ occupiedSlots.delete(id);
129
+ if (lastHit !== null)
130
+ compute(lastHit);
131
+ },
79
132
  commit() {
80
133
  if (preview === null || !preview.valid)
81
134
  return null;
135
+ if (preview.slotId !== undefined)
136
+ occupiedSlots.add(preview.slotId);
82
137
  return {
83
138
  center: preview.center,
84
139
  y: preview.y,
@@ -86,6 +141,7 @@ export function createPlacementController(config) {
86
141
  quarterTurns,
87
142
  footprint: preview.footprint,
88
143
  aabb: preview.aabb,
144
+ ...(preview.slotId !== undefined ? { slotId: preview.slotId } : {}),
89
145
  };
90
146
  },
91
147
  reset() {
@@ -1,4 +1,4 @@
1
- import type { TerrainEnvironmentDescriptor, WorldBounds } 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;
@@ -46,7 +46,68 @@ export interface ArenaFieldConfig {
46
46
  }
47
47
  export declare const ARENA_WATER_LEVEL = -0.9;
48
48
  export declare function arenaField(config?: ArenaFieldConfig): TerrainField;
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];
73
+ export interface TerrainPalette {
74
+ low: string;
75
+ high: string;
76
+ waterline: string;
77
+ }
78
+ export declare const DEFAULT_TERRAIN_MATERIAL: TerrainMaterial;
79
+ export declare const TERRAIN_MATERIAL_PALETTES: Record<TerrainMaterial, TerrainPalette>;
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;
49
97
  export declare function resolveTerrainField(descriptor?: TerrainEnvironmentDescriptor): TerrainField;
98
+ /** Returns `position` with `y` replaced by the field's ground height (plus `offset`) at its `x`/`z`. */
99
+ export declare function snapToGround(field: TerrainField, position: readonly [number, number, number], offset?: number): [number, number, number];
100
+ export interface GroundSnapTarget {
101
+ position: readonly [number, number, number];
102
+ }
103
+ export interface GroundSnapEntityStore {
104
+ get(id: string): GroundSnapTarget | null;
105
+ setPose(id: string, pose: {
106
+ position?: readonly [number, number, number];
107
+ }): boolean;
108
+ }
109
+ /** Ground-snaps an already-spawned entity in place; returns false when `id` is unknown. */
110
+ export declare function snapEntityToGround(entities: GroundSnapEntityStore, id: string, field: TerrainField, offset?: number): boolean;
50
111
  export declare const DEFAULT_MAX_WALK_SLOPE = 0.6;
51
112
  export declare function resolveGroundStep(field: TerrainField, x: number, z: number, stepX: number, stepZ: number, maxSlope?: number): {
52
113
  stepX: number;
@@ -148,20 +148,264 @@ export function arenaField(config = {}) {
148
148
  };
149
149
  return fieldFromHeight(sampleHeight, { bounds: config.bounds, waterLevel: ARENA_WATER_LEVEL });
150
150
  }
151
+ export function groundFieldFor(world) {
152
+ if (world !== undefined && world.kind === "environment")
153
+ return resolveEnvironmentField(world);
154
+ return flatField();
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
+ }
229
+ export const DEFAULT_TERRAIN_MATERIAL = "grass";
230
+ export const TERRAIN_MATERIAL_PALETTES = {
231
+ grass: { low: "#30402c", high: "#7f8b50", waterline: "#1d4c6e" },
232
+ sand: { low: "#9c8354", high: "#e0c98a", waterline: "#2f6f8f" },
233
+ snow: { low: "#c7d3dc", high: "#ffffff", waterline: "#3a6ea5" },
234
+ rock: { low: "#3a3a3d", high: "#8a8a8d", waterline: "#1d4c6e" },
235
+ ash: { low: "#2b2622", high: "#5c534a", waterline: "#3a3630" },
236
+ highland: { low: "#414f33", high: "#a3a86b", waterline: "#365f63" },
237
+ slate: { low: "#2f3540", high: "#7d8896", waterline: "#26404f" },
238
+ };
239
+ export function resolveTerrainPalette(descriptor = {}) {
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
+ }
245
+ const colors = descriptor.colors ?? {};
246
+ return {
247
+ low: colors.low ?? preset.low,
248
+ high: colors.high ?? preset.high,
249
+ waterline: colors.waterline ?? preset.waterline,
250
+ };
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
+ }
310
+ function withFlattenMasks(sampleHeight, masks) {
311
+ const resolved = masks.map((mask) => ({
312
+ center: mask.center,
313
+ radius: mask.radius,
314
+ target: mask.height ?? sampleHeight(mask.center[0], mask.center[1]),
315
+ falloff: mask.falloff ?? mask.radius * 0.5,
316
+ }));
317
+ return (x, z) => {
318
+ const base = sampleHeight(x, z);
319
+ let height = base;
320
+ for (const mask of resolved) {
321
+ const distance = Math.hypot(x - mask.center[0], z - mask.center[1]);
322
+ if (distance <= mask.radius) {
323
+ height = mask.target;
324
+ }
325
+ else if (distance <= mask.radius + mask.falloff) {
326
+ const t = smoothstep(mask.radius, mask.radius + mask.falloff, distance);
327
+ height = lerp(mask.target, base, t);
328
+ }
329
+ }
330
+ return height;
331
+ };
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
+ }
151
369
  export function resolveTerrainField(descriptor) {
152
370
  if (descriptor === undefined)
153
371
  return flatField();
154
- return noiseField({
155
- seed: descriptor.seed,
156
- amplitude: descriptor.height,
157
- frequency: descriptor.frequency,
158
- octaves: descriptor.octaves,
159
- ridged: descriptor.ridged,
160
- baseHeight: descriptor.baseHeight,
161
- waterLevel: descriptor.waterLevel,
162
- bounds: descriptor.bounds,
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
+ });
390
+ if (descriptor.flatten === undefined || descriptor.flatten.length === 0)
391
+ return base;
392
+ return fieldFromHeight(withFlattenMasks(base.sampleHeight, descriptor.flatten), {
393
+ bounds: base.bounds,
394
+ waterLevel: base.waterLevel,
163
395
  });
164
396
  }
397
+ /** Returns `position` with `y` replaced by the field's ground height (plus `offset`) at its `x`/`z`. */
398
+ export function snapToGround(field, position, offset = 0) {
399
+ const [x, , z] = position;
400
+ return [x, field.sampleHeight(x, z) + offset, z];
401
+ }
402
+ /** Ground-snaps an already-spawned entity in place; returns false when `id` is unknown. */
403
+ export function snapEntityToGround(entities, id, field, offset = 0) {
404
+ const entity = entities.get(id);
405
+ if (entity === null)
406
+ return false;
407
+ return entities.setPose(id, { position: snapToGround(field, entity.position, offset) });
408
+ }
165
409
  export const DEFAULT_MAX_WALK_SLOPE = 0.6;
166
410
  export function resolveGroundStep(field, x, z, stepX, stepZ, maxSlope = DEFAULT_MAX_WALK_SLOPE) {
167
411
  const baseHeight = field.sampleHeight(x, z);
@@ -0,0 +1,54 @@
1
+ export type VoxelCoord = readonly [number, number, number];
2
+ export type VoxelFace = "px" | "nx" | "py" | "ny" | "pz" | "nz";
3
+ export interface VoxelFieldConfig {
4
+ chunkSize?: number;
5
+ }
6
+ export interface VoxelCell<T extends string = string> {
7
+ x: number;
8
+ y: number;
9
+ z: number;
10
+ type: T;
11
+ }
12
+ export interface VoxelHit<T extends string = string> {
13
+ x: number;
14
+ y: number;
15
+ z: number;
16
+ type: T;
17
+ face: VoxelFace;
18
+ adjacent: VoxelCoord;
19
+ distance: number;
20
+ }
21
+ export interface VoxelBounds {
22
+ min: VoxelCoord;
23
+ max: VoxelCoord;
24
+ }
25
+ export interface VoxelFieldSummary<T extends string = string> {
26
+ blocks: number;
27
+ types: Record<T, number>;
28
+ bounds: VoxelBounds | null;
29
+ }
30
+ export interface VoxelField<T extends string = string> {
31
+ set(x: number, y: number, z: number, type: T): boolean;
32
+ remove(x: number, y: number, z: number): boolean;
33
+ get(x: number, y: number, z: number): T | null;
34
+ has(x: number, y: number, z: number): boolean;
35
+ fill(min: VoxelCoord, max: VoxelCoord, type: T): number;
36
+ clear(): void;
37
+ count(): number;
38
+ cells(): IterableIterator<VoxelCell<T>>;
39
+ bounds(): VoxelBounds | null;
40
+ neighbors(x: number, y: number, z: number): VoxelCell<T>[];
41
+ exposedFaces(x: number, y: number, z: number): readonly VoxelFace[];
42
+ raycast(origin: VoxelCoord, direction: VoxelCoord, maxDistance: number): VoxelHit<T> | null;
43
+ chunkOf(x: number, y: number, z: number): {
44
+ cx: number;
45
+ cy: number;
46
+ cz: number;
47
+ };
48
+ chunkVersion(cx: number, cy: number, cz: number): number;
49
+ subscribe(listener: () => void): () => void;
50
+ summary(): VoxelFieldSummary<T>;
51
+ }
52
+ export declare const VOXEL_FACES: readonly VoxelFace[];
53
+ export declare const VOXEL_FACE_NORMALS: Record<VoxelFace, VoxelCoord>;
54
+ export declare function createVoxelField<T extends string = string>(config?: VoxelFieldConfig): VoxelField<T>;