@jgengine/core 0.18.1 → 0.19.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 (220) hide show
  1. package/CHANGELOG.md +158 -7
  2. package/dist/ai/decisionGraph.d.ts +48 -4
  3. package/dist/ai/decisionGraph.js +153 -23
  4. package/dist/ai/groupAssist.d.ts +10 -0
  5. package/dist/ai/groupAssist.js +11 -0
  6. package/dist/ai/pursuit.d.ts +1 -1
  7. package/dist/ai/pursuit.js +1 -1
  8. package/dist/ai/threat.d.ts +8 -0
  9. package/dist/ai/threat.js +10 -0
  10. package/dist/anim/animGraph.d.ts +11 -0
  11. package/dist/anim/animGraph.js +128 -0
  12. package/dist/anim/boneTexture.d.ts +55 -0
  13. package/dist/anim/boneTexture.js +43 -0
  14. package/dist/anim/footPlacement.d.ts +57 -0
  15. package/dist/anim/footPlacement.js +67 -0
  16. package/dist/anim/locomotionGraph.d.ts +9 -0
  17. package/dist/anim/locomotionGraph.js +29 -0
  18. package/dist/audio/audioFalloff.d.ts +2 -0
  19. package/dist/audio/doppler.d.ts +21 -0
  20. package/dist/audio/doppler.js +32 -0
  21. package/dist/audio/engineLayers.d.ts +83 -0
  22. package/dist/audio/engineLayers.js +115 -0
  23. package/dist/combat/abilityKit.d.ts +1 -0
  24. package/dist/combat/abilityKit.js +1 -0
  25. package/dist/combat/death.d.ts +7 -0
  26. package/dist/combat/shotOrigin.d.ts +6 -0
  27. package/dist/combat/shotOrigin.js +6 -0
  28. package/dist/combat/weaponFeedback.d.ts +36 -0
  29. package/dist/combat/weaponFeedback.js +42 -0
  30. package/dist/combat/weaponHandling.d.ts +161 -0
  31. package/dist/combat/weaponHandling.js +202 -0
  32. package/dist/combat/weaponPresentation.d.ts +115 -0
  33. package/dist/combat/weaponPresentation.js +96 -0
  34. package/dist/devtools/warnOnce.d.ts +8 -0
  35. package/dist/devtools/warnOnce.js +18 -0
  36. package/dist/editor/document.js +3 -1
  37. package/dist/editor/environment.js +8 -0
  38. package/dist/editor/types.d.ts +4 -0
  39. package/dist/faction/reputation.d.ts +6 -0
  40. package/dist/faction/reputation.js +11 -0
  41. package/dist/game/cameraConfig.d.ts +51 -1
  42. package/dist/game/events.d.ts +6 -0
  43. package/dist/game/lootTable.d.ts +4 -0
  44. package/dist/game/lootTable.js +16 -0
  45. package/dist/game/playableGame.d.ts +84 -17
  46. package/dist/game/toasts.d.ts +8 -0
  47. package/dist/game/toasts.js +7 -0
  48. package/dist/game/vfxInstance.d.ts +9 -0
  49. package/dist/game/vfxInstance.js +12 -1
  50. package/dist/game/viewports.d.ts +43 -0
  51. package/dist/game/viewports.js +46 -0
  52. package/dist/game/worldItem.d.ts +5 -1
  53. package/dist/game/worldItem.js +0 -1
  54. package/dist/input/actionBindings.d.ts +15 -1
  55. package/dist/input/actionBindings.js +14 -3
  56. package/dist/input/actionContexts.d.ts +25 -1
  57. package/dist/input/actionContexts.js +82 -25
  58. package/dist/input/axisInput.d.ts +4 -0
  59. package/dist/input/axisInput.js +4 -0
  60. package/dist/input/axisShaper.d.ts +73 -0
  61. package/dist/input/axisShaper.js +105 -0
  62. package/dist/input/gamepadModel.d.ts +36 -2
  63. package/dist/input/gamepadModel.js +69 -30
  64. package/dist/input/haptics.d.ts +50 -0
  65. package/dist/input/haptics.js +74 -0
  66. package/dist/input/inputBuffer.d.ts +40 -0
  67. package/dist/input/inputBuffer.js +59 -0
  68. package/dist/input/lookChannel.d.ts +10 -0
  69. package/dist/input/lookChannel.js +6 -0
  70. package/dist/input/pointer.d.ts +2 -0
  71. package/dist/input/pointer.js +4 -0
  72. package/dist/item/affix.d.ts +6 -0
  73. package/dist/item/affix.js +6 -0
  74. package/dist/meta/changelog.d.ts +1 -1
  75. package/dist/meta/changelog.js +112 -1
  76. package/dist/movement/glideModel.d.ts +3 -0
  77. package/dist/movement/glideModel.js +6 -0
  78. package/dist/movement/grappleSwing.d.ts +2 -0
  79. package/dist/movement/grappleSwing.js +5 -1
  80. package/dist/movement/movementModel.d.ts +39 -0
  81. package/dist/movement/movementModel.js +62 -19
  82. package/dist/movement/movementProbe.d.ts +51 -0
  83. package/dist/movement/movementProbe.js +119 -0
  84. package/dist/movement/playerMovement.d.ts +21 -2
  85. package/dist/movement/playerMovement.js +89 -6
  86. package/dist/movement/solidObstacles.d.ts +9 -1
  87. package/dist/movement/solidObstacles.js +44 -2
  88. package/dist/movement/voxelController.js +7 -7
  89. package/dist/nav/navFromEnvironment.d.ts +9 -3
  90. package/dist/nav/navFromEnvironment.js +18 -12
  91. package/dist/nav/navMesh.d.ts +43 -2
  92. package/dist/nav/navMesh.js +534 -128
  93. package/dist/physics/aircraftDynamics.d.ts +390 -0
  94. package/dist/physics/aircraftDynamics.js +704 -0
  95. package/dist/physics/boatDynamics.d.ts +116 -0
  96. package/dist/physics/boatDynamics.js +160 -0
  97. package/dist/physics/drivableVehicle.d.ts +22 -8
  98. package/dist/physics/drivableVehicle.js +4 -2
  99. package/dist/physics/flightDynamics.d.ts +21 -4
  100. package/dist/physics/flightDynamics.js +28 -5
  101. package/dist/physics/handlingProbe.d.ts +249 -0
  102. package/dist/physics/handlingProbe.js +606 -0
  103. package/dist/physics/kinematicVehicle.d.ts +32 -2
  104. package/dist/physics/kinematicVehicle.js +15 -0
  105. package/dist/physics/physicsBackend.d.ts +12 -0
  106. package/dist/physics/physicsBackendConformance.js +34 -0
  107. package/dist/physics/physicsWorldBackend.d.ts +1 -0
  108. package/dist/physics/physicsWorldBackend.js +33 -0
  109. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  110. package/dist/physics/vehicleBackendLink.js +102 -0
  111. package/dist/physics/vehicleDynamics.d.ts +374 -0
  112. package/dist/physics/vehicleDynamics.js +737 -0
  113. package/dist/physics/worldColliders.d.ts +1 -1
  114. package/dist/physics/worldColliders.js +35 -3
  115. package/dist/puzzle/cellGrid.d.ts +15 -9
  116. package/dist/puzzle/cellGrid.js +13 -9
  117. package/dist/render/lookPreset.d.ts +9 -2
  118. package/dist/render/lookPreset.js +29 -2
  119. package/dist/render/postProcessing.d.ts +30 -0
  120. package/dist/render/postProcessing.js +1 -1
  121. package/dist/runtime/cameraDirector.d.ts +17 -3
  122. package/dist/runtime/cameraDirector.js +25 -0
  123. package/dist/runtime/commandInput.d.ts +37 -0
  124. package/dist/runtime/commandInput.js +58 -0
  125. package/dist/runtime/commandRunner.d.ts +27 -0
  126. package/dist/runtime/commandRunner.js +31 -3
  127. package/dist/runtime/context/combat.d.ts +1 -0
  128. package/dist/runtime/context/combat.js +2 -1
  129. package/dist/runtime/context/deathLoot.d.ts +2 -0
  130. package/dist/runtime/context/deathLoot.js +1 -0
  131. package/dist/runtime/gameContext.js +7 -0
  132. package/dist/runtime/gameContextTypes.d.ts +19 -3
  133. package/dist/runtime/gameRuntime.d.ts +3 -1
  134. package/dist/runtime/gameRuntime.js +4 -0
  135. package/dist/runtime/inputSnapshot.d.ts +6 -0
  136. package/dist/runtime/inputSnapshot.js +10 -0
  137. package/dist/runtime/localPlayers.d.ts +73 -0
  138. package/dist/runtime/localPlayers.js +119 -0
  139. package/dist/runtime/transport.d.ts +9 -0
  140. package/dist/runtime/transport.js +7 -1
  141. package/dist/scene/behaviorRuntime.d.ts +12 -2
  142. package/dist/scene/behaviorRuntime.js +30 -14
  143. package/dist/scene/behaviors.d.ts +5 -1
  144. package/dist/scene/sceneKinds.d.ts +9 -0
  145. package/dist/scene/vehicleSeat.d.ts +5 -1
  146. package/dist/scene/vehicleSeat.js +5 -1
  147. package/dist/sensor/concealment.d.ts +6 -0
  148. package/dist/sensor/concealment.js +8 -0
  149. package/dist/sensor/freezeMonitor.d.ts +6 -0
  150. package/dist/sensor/freezeMonitor.js +8 -0
  151. package/dist/sensor/perception.d.ts +11 -1
  152. package/dist/sensor/perception.js +57 -10
  153. package/dist/sensor/recordingBuffer.d.ts +3 -0
  154. package/dist/sensor/recordingBuffer.js +8 -0
  155. package/dist/session/roundState.d.ts +4 -0
  156. package/dist/session/roundState.js +11 -0
  157. package/dist/settings/frameRateLimit.d.ts +16 -0
  158. package/dist/settings/frameRateLimit.js +37 -0
  159. package/dist/settings/graphicsProfile.d.ts +2 -0
  160. package/dist/settings/graphicsProfile.js +12 -0
  161. package/dist/settings/graphicsSettings.d.ts +40 -0
  162. package/dist/settings/graphicsSettings.js +81 -0
  163. package/dist/settings/settingsModel.d.ts +9 -1
  164. package/dist/settings/settingsModel.js +10 -0
  165. package/dist/stats/eventMeter.d.ts +1 -1
  166. package/dist/stats/eventMeter.js +1 -1
  167. package/dist/vfx/feedbackMixer.d.ts +61 -0
  168. package/dist/vfx/feedbackMixer.js +106 -0
  169. package/dist/visibility/simulationCulling.d.ts +8 -1
  170. package/dist/visibility/simulationCulling.js +7 -0
  171. package/dist/visibility/spatialIndex.d.ts +21 -0
  172. package/dist/visibility/spatialIndex.js +16 -0
  173. package/dist/world/authoredSolids.d.ts +46 -0
  174. package/dist/world/authoredSolids.js +75 -0
  175. package/dist/world/buildingGenerator.js +2 -2
  176. package/dist/world/buildingKit.d.ts +3 -1
  177. package/dist/world/buildings.d.ts +34 -2
  178. package/dist/world/buildings.js +11 -1
  179. package/dist/world/cityKind.d.ts +7 -0
  180. package/dist/world/cityKind.js +52 -1
  181. package/dist/world/features.d.ts +29 -2
  182. package/dist/world/features.js +6 -0
  183. package/dist/world/footprintGrid.d.ts +7 -0
  184. package/dist/world/footprintGrid.js +16 -0
  185. package/dist/world/interiors.d.ts +1 -0
  186. package/dist/world/interiors.js +5 -3
  187. package/dist/world/lod.d.ts +6 -0
  188. package/dist/world/lod.js +8 -0
  189. package/dist/world/terraform.d.ts +8 -0
  190. package/dist/world/terraform.js +7 -0
  191. package/dist/world/walls.d.ts +7 -0
  192. package/dist/world/walls.js +7 -0
  193. package/dist/world/worldSolids.d.ts +84 -0
  194. package/dist/world/worldSolids.js +240 -0
  195. package/dist/world.d.ts +2 -2
  196. package/dist/world.js +1 -1
  197. package/package.json +1 -1
  198. package/skills/ce-handoff/SKILL.md +1 -1
  199. package/skills/game-design/SKILL.md +2 -2
  200. package/skills/jgengine/SKILL.md +7 -5
  201. package/skills/jgengine/api.md +118 -89
  202. package/skills/jgengine/capabilities.md +28 -0
  203. package/skills/jgengine/recipes/minimal-game.md +3 -0
  204. package/skills/jgengine-combat/SKILL.md +5 -0
  205. package/skills/jgengine-combat/api.md +138 -103
  206. package/skills/jgengine-combat/capabilities.md +29 -1
  207. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  208. package/skills/jgengine-gameplay/api.md +462 -436
  209. package/skills/jgengine-gameplay/capabilities.md +24 -0
  210. package/skills/jgengine-gameplay/reference-systems.md +4 -0
  211. package/skills/jgengine-world/SKILL.md +15 -3
  212. package/skills/jgengine-world/api.md +873 -626
  213. package/skills/jgengine-world/capabilities.md +130 -6
  214. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  215. package/skills/jgengine-world/recipes/controls.md +44 -0
  216. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  217. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  218. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  219. package/skills/jgengine-world/reference.md +31 -2
  220. package/skills/jgengine-world/visibility.md +112 -0
@@ -1,6 +1,9 @@
1
1
  function cellKey(cell) {
2
2
  return `${cell.col}:${cell.row}`;
3
3
  }
4
+ function copyReservation(reservation) {
5
+ return { id: reservation.id, kind: reservation.kind, cells: reservation.cells.map((cell) => ({ col: cell.col, row: cell.row })) };
6
+ }
4
7
  /**
5
8
  * Multi-cell footprint occupancy/reservation on a shared build grid — `world/placementController`
6
9
  * only owns the ghost preview; this is the persistent claim a committed placement holds so the next
@@ -70,6 +73,19 @@ export function createFootprintGrid(options = {}) {
70
73
  occupied.clear();
71
74
  reservations.clear();
72
75
  },
76
+ snapshot() {
77
+ return { reservations: Array.from(reservations.values(), copyReservation) };
78
+ },
79
+ restore(next) {
80
+ occupied.clear();
81
+ reservations.clear();
82
+ for (const reservation of next.reservations) {
83
+ const copy = copyReservation(reservation);
84
+ reservations.set(copy.id, copy);
85
+ for (const cell of copy.cells)
86
+ occupied.set(cellKey(cell), copy.id);
87
+ }
88
+ },
73
89
  };
74
90
  }
75
91
  const CARDINAL_OFFSETS = [
@@ -32,4 +32,5 @@ export interface Interiors {
32
32
  toInterior(id: string, exterior: Vec2): Vec2 | null;
33
33
  toExterior(id: string, interior: Vec2): Vec2 | null;
34
34
  }
35
+ /** Frame conversion and bounded movement across an exterior and its interiors; holds config only, no play state. */
35
36
  export declare function createInteriors(config?: InteriorsConfig): Interiors;
@@ -14,12 +14,14 @@ function toExteriorFrame(interior, local) {
14
14
  const rotated = rotate(local, interior.rotation ?? 0);
15
15
  return [rotated[0] + interior.origin[0], rotated[1] + interior.origin[1]];
16
16
  }
17
+ function indexInteriors(interiors) {
18
+ return new Map(interiors.map((interior) => [interior.id, interior]));
19
+ }
20
+ /** Frame conversion and bounded movement across an exterior and its interiors; holds config only, no play state. */
17
21
  export function createInteriors(config = {}) {
18
22
  const radius = config.radius ?? 0;
19
23
  const exterior = config.exterior ?? {};
20
- const byId = new Map();
21
- for (const interior of config.interiors ?? [])
22
- byId.set(interior.id, interior);
24
+ const byId = indexInteriors(config.interiors ?? []);
23
25
  return {
24
26
  move(location, delta) {
25
27
  if (location.space.kind === "exterior") {
@@ -7,11 +7,17 @@ export interface LodSchedulerConfig {
7
7
  beyondInterval?: number | null;
8
8
  stagger?: boolean;
9
9
  }
10
+ /** Plain JSON state of a {@link LodScheduler}: each id's accumulated seconds. */
11
+ export interface LodSchedulerState {
12
+ buckets: Record<string, number>;
13
+ }
10
14
  export interface LodScheduler {
11
15
  bandIndex(distance: number): number;
12
16
  step(id: string, distance: number, dtSeconds: number): number;
13
17
  remove(id: string): void;
14
18
  clear(): void;
15
19
  size(): number;
20
+ snapshot(): LodSchedulerState;
21
+ restore(next: LodSchedulerState): void;
16
22
  }
17
23
  export declare function createLodScheduler(config: LodSchedulerConfig): LodScheduler;
package/dist/world/lod.js CHANGED
@@ -62,5 +62,13 @@ export function createLodScheduler(config) {
62
62
  size() {
63
63
  return buckets.size;
64
64
  },
65
+ snapshot() {
66
+ return { buckets: Object.fromEntries(buckets) };
67
+ },
68
+ restore(next) {
69
+ buckets.clear();
70
+ for (const [id, seconds] of Object.entries(next.buckets))
71
+ buckets.set(id, seconds);
72
+ },
65
73
  };
66
74
  }
@@ -262,5 +262,13 @@ export interface TerraformBrush {
262
262
  config(): Required<Omit<TerraformBrushConfig, "surface">> & {
263
263
  surface: string | undefined;
264
264
  };
265
+ /** The retunable brush settings; the terrain keeps its own `snapshot()`. */
266
+ snapshot(): TerraformBrushState;
267
+ restore(next: TerraformBrushState): void;
268
+ }
269
+ /** Plain JSON state of a {@link TerraformBrush}. */
270
+ export interface TerraformBrushState {
271
+ radius: number;
272
+ strength: number;
265
273
  }
266
274
  export declare function createTerraformBrush(terrain: Pick<EditableTerrain, "apply">, config?: TerraformBrushConfig): TerraformBrush;
@@ -787,5 +787,12 @@ export function createTerraformBrush(terrain, config = {}) {
787
787
  config() {
788
788
  return { radius, strength, falloff, surface };
789
789
  },
790
+ snapshot() {
791
+ return { radius, strength };
792
+ },
793
+ restore(next) {
794
+ radius = next.radius;
795
+ strength = next.strength;
796
+ },
790
797
  };
791
798
  }
@@ -50,6 +50,11 @@ export interface SurfacePaintStore {
50
50
  }
51
51
  /** @internal */
52
52
  export declare function createSurfacePaint(): SurfacePaintStore;
53
+ /** Plain JSON state of a {@link WallDrawTool}: the drawn points and whether the loop is closed. */
54
+ export interface WallDrawToolState {
55
+ points: Vec2[];
56
+ closed: boolean;
57
+ }
53
58
  export interface WallDrawTool {
54
59
  addPoint(point: Vec2, snap?: number): Vec2;
55
60
  undo(): void;
@@ -60,6 +65,8 @@ export interface WallDrawTool {
60
65
  isClosed(): boolean;
61
66
  footprint(): EnclosedFootprint | null;
62
67
  roof(config?: RoofConfig): RoofPlan | null;
68
+ snapshot(): WallDrawToolState;
69
+ restore(next: WallDrawToolState): void;
63
70
  }
64
71
  /** @internal */
65
72
  export declare function createWallDrawTool(config?: {
@@ -220,6 +220,13 @@ export function createWallDrawTool(config = {}) {
220
220
  const fp = this.footprint();
221
221
  return fp === null ? null : autoRoof(fp, roofConfig);
222
222
  },
223
+ snapshot() {
224
+ return { points: pts.map((point) => [point[0], point[1]]), closed };
225
+ },
226
+ restore(next) {
227
+ pts = next.points.map((point) => [point[0], point[1]]);
228
+ closed = next.closed;
229
+ },
223
230
  };
224
231
  }
225
232
  /** @internal */
@@ -0,0 +1,84 @@
1
+ import type { GeneratedBuilding } from "./buildings.js";
2
+ import type { EnvironmentWorldFeature } from "./features.js";
3
+ import type { Aabb } from "./geometry.js";
4
+ import type { WallSegment } from "./walls.js";
5
+ /** One solid box in world space: the collision half of generated or authored world geometry. */
6
+ export interface WorldSolid {
7
+ center: readonly [number, number, number];
8
+ halfExtents: readonly [number, number, number];
9
+ /** Yaw about `center`, radians, in the engine's `rotationY` convention. Default `0`. */
10
+ rotationY?: number;
11
+ }
12
+ /** Serializable contents of a {@link WorldSolids}: every layer's solids by layer id. */
13
+ export interface WorldSolidsState {
14
+ layers: Record<string, readonly WorldSolid[]>;
15
+ }
16
+ /**
17
+ * Static collision for world geometry that is not a scene object: generated buildings, studio
18
+ * volumes like `city`, wall runs. Solids live in named layers so a feature can replace or drop its
19
+ * own set without touching the rest. `solidObstaclesNear` (player and NPC movement),
20
+ * `syncWorldColliders` (physics backends) and `populateNavGridFromSolids` all read this one store.
21
+ */
22
+ export interface WorldSolids {
23
+ /** Replace a layer's solids; an empty list removes the layer. */
24
+ set(layer: string, solids: readonly WorldSolid[]): void;
25
+ remove(layer: string): void;
26
+ /** Run several `set`/`remove` calls as one change: one index rebuild, one notification. */
27
+ batch(apply: () => void): void;
28
+ /** Solids in one layer, empty when absent. */
29
+ layer(layer: string): readonly WorldSolid[];
30
+ layers(): readonly string[];
31
+ /** Every solid whose world AABB overlaps `min`..`max`. Bounded by a uniform XZ hash grid. */
32
+ inBox(min: readonly [number, number, number], max: readonly [number, number, number]): WorldSolid[];
33
+ /** Every solid in every layer. */
34
+ all(): WorldSolid[];
35
+ count(): number;
36
+ /** Bumps on every change; cheap cache key for consumers. */
37
+ version(): number;
38
+ subscribe(listener: () => void): () => void;
39
+ snapshot(): WorldSolidsState;
40
+ restore(state: WorldSolidsState): void;
41
+ }
42
+ /**
43
+ * Yaw-expanded world AABB of an oriented solid box.
44
+ * @capability world-solid-bounds world AABB and nav footprint of a world solid
45
+ */
46
+ export declare function worldSolidBounds(solid: WorldSolid): {
47
+ min: [number, number, number];
48
+ max: [number, number, number];
49
+ };
50
+ /**
51
+ * XZ footprint of a solid's world AABB, for 2D consumers such as nav grids.
52
+ * @capability world-solid-bounds world AABB and nav footprint of a world solid
53
+ */
54
+ export declare function worldSolidFootprint(solid: WorldSolid): Aabb;
55
+ /**
56
+ * A store of static world solids in named layers.
57
+ * @capability world-solids collision for generated and authored world geometry (buildings, city lots, walls)
58
+ */
59
+ export declare function createWorldSolids(options?: {
60
+ cellSize?: number;
61
+ }): WorldSolids;
62
+ /**
63
+ * One solid per generated building: its `bounds` footprint turned by `rotationY` about `center`,
64
+ * from the ground under its center up through its floors. `groundHeight` defaults to flat `0`.
65
+ * @capability world-solids-from-data derive collision boxes from generated buildings and wall runs
66
+ */
67
+ export declare function buildingSolids(buildings: readonly GeneratedBuilding[], groundHeight?: (x: number, z: number) => number): WorldSolid[];
68
+ /**
69
+ * Solids for every `structures` descriptor on an environment feature, skipping any marked `solid: false`.
70
+ * @capability world-solids-from-data derive collision boxes from generated buildings and wall runs
71
+ */
72
+ export declare function structureSolids(world: EnvironmentWorldFeature, groundHeight?: (x: number, z: number) => number): WorldSolid[];
73
+ /** Wall height, thickness and base for {@link wallSolids}. */
74
+ export interface WallSolidOptions {
75
+ height: number;
76
+ thickness: number;
77
+ /** Ground height under a point; the wall stands on it at each segment's midpoint. Default flat `0`. */
78
+ groundHeight?: (x: number, z: number) => number;
79
+ }
80
+ /**
81
+ * One oriented solid per wall segment (see `wallSegments` in `world/walls`).
82
+ * @capability world-solids-from-data derive collision boxes from generated buildings and wall runs
83
+ */
84
+ export declare function wallSolids(segments: readonly WallSegment[], options: WallSolidOptions): WorldSolid[];
@@ -0,0 +1,240 @@
1
+ import { resolveStructureBuildings } from "./environmentSummary.js";
2
+ const DEFAULT_CELL_SIZE = 16;
3
+ /** A solid covering more cells than this goes on an always-checked list instead of flooding the grid. */
4
+ const MAX_CELLS_PER_SOLID = 256;
5
+ const CELL_OFFSET = 32768;
6
+ /**
7
+ * Yaw-expanded world AABB of an oriented solid box.
8
+ * @capability world-solid-bounds world AABB and nav footprint of a world solid
9
+ */
10
+ export function worldSolidBounds(solid) {
11
+ const yaw = solid.rotationY ?? 0;
12
+ const cos = Math.abs(Math.cos(yaw));
13
+ const sin = Math.abs(Math.sin(yaw));
14
+ const [hx, hy, hz] = solid.halfExtents;
15
+ const ex = hx * cos + hz * sin;
16
+ const ez = hx * sin + hz * cos;
17
+ const [cx, cy, cz] = solid.center;
18
+ return { min: [cx - ex, cy - hy, cz - ez], max: [cx + ex, cy + hy, cz + ez] };
19
+ }
20
+ /**
21
+ * XZ footprint of a solid's world AABB, for 2D consumers such as nav grids.
22
+ * @capability world-solid-bounds world AABB and nav footprint of a world solid
23
+ */
24
+ export function worldSolidFootprint(solid) {
25
+ const { min, max } = worldSolidBounds(solid);
26
+ return { minX: min[0], minZ: min[2], maxX: max[0], maxZ: max[2] };
27
+ }
28
+ /**
29
+ * A store of static world solids in named layers.
30
+ * @capability world-solids collision for generated and authored world geometry (buildings, city lots, walls)
31
+ */
32
+ export function createWorldSolids(options = {}) {
33
+ const cellSize = options.cellSize ?? DEFAULT_CELL_SIZE;
34
+ const layerMap = new Map();
35
+ const grid = new Map();
36
+ const oversize = [];
37
+ const listeners = new Set();
38
+ let stamp = 0;
39
+ let total = 0;
40
+ let revision = 0;
41
+ let batchDepth = 0;
42
+ let pending = false;
43
+ const cellOf = (value) => Math.floor(value / cellSize);
44
+ const keyOf = (ix, iz) => (ix + CELL_OFFSET) * 65536 + (iz + CELL_OFFSET);
45
+ function index(entry) {
46
+ const x0 = cellOf(entry.minX);
47
+ const x1 = cellOf(entry.maxX);
48
+ const z0 = cellOf(entry.minZ);
49
+ const z1 = cellOf(entry.maxZ);
50
+ if ((x1 - x0 + 1) * (z1 - z0 + 1) > MAX_CELLS_PER_SOLID) {
51
+ oversize.push(entry);
52
+ return;
53
+ }
54
+ for (let ix = x0; ix <= x1; ix += 1) {
55
+ for (let iz = z0; iz <= z1; iz += 1) {
56
+ const key = keyOf(ix, iz);
57
+ const bucket = grid.get(key);
58
+ if (bucket === undefined)
59
+ grid.set(key, [entry]);
60
+ else
61
+ bucket.push(entry);
62
+ }
63
+ }
64
+ }
65
+ function rebuildIndex() {
66
+ grid.clear();
67
+ oversize.length = 0;
68
+ total = 0;
69
+ for (const entries of layerMap.values()) {
70
+ for (const entry of entries)
71
+ index(entry);
72
+ total += entries.length;
73
+ }
74
+ }
75
+ function changed() {
76
+ if (batchDepth > 0) {
77
+ pending = true;
78
+ return;
79
+ }
80
+ pending = false;
81
+ rebuildIndex();
82
+ revision += 1;
83
+ for (const listener of listeners)
84
+ listener();
85
+ }
86
+ function indexed(solid) {
87
+ const { min, max } = worldSolidBounds(solid);
88
+ return { solid, minX: min[0], minY: min[1], minZ: min[2], maxX: max[0], maxY: max[1], maxZ: max[2], stamp: 0 };
89
+ }
90
+ function overlaps(entry, min, max) {
91
+ return (entry.maxX >= min[0] &&
92
+ entry.minX <= max[0] &&
93
+ entry.maxY >= min[1] &&
94
+ entry.minY <= max[1] &&
95
+ entry.maxZ >= min[2] &&
96
+ entry.minZ <= max[2]);
97
+ }
98
+ return {
99
+ set(layer, solids) {
100
+ if (solids.length === 0) {
101
+ if (!layerMap.delete(layer))
102
+ return;
103
+ }
104
+ else {
105
+ layerMap.set(layer, solids.map(indexed));
106
+ }
107
+ changed();
108
+ },
109
+ remove(layer) {
110
+ if (layerMap.delete(layer))
111
+ changed();
112
+ },
113
+ batch(apply) {
114
+ batchDepth += 1;
115
+ try {
116
+ apply();
117
+ }
118
+ finally {
119
+ batchDepth -= 1;
120
+ }
121
+ if (batchDepth === 0 && pending)
122
+ changed();
123
+ },
124
+ layer: (layer) => layerMap.get(layer)?.map((entry) => entry.solid) ?? [],
125
+ layers: () => [...layerMap.keys()],
126
+ inBox(min, max) {
127
+ stamp += 1;
128
+ const out = [];
129
+ if (total === 0)
130
+ return out;
131
+ const x0 = cellOf(min[0]);
132
+ const x1 = cellOf(max[0]);
133
+ const z0 = cellOf(min[2]);
134
+ const z1 = cellOf(max[2]);
135
+ if ((x1 - x0 + 1) * (z1 - z0 + 1) > grid.size) {
136
+ for (const entries of layerMap.values()) {
137
+ for (const entry of entries)
138
+ if (overlaps(entry, min, max))
139
+ out.push(entry.solid);
140
+ }
141
+ return out;
142
+ }
143
+ for (let ix = x0; ix <= x1; ix += 1) {
144
+ for (let iz = z0; iz <= z1; iz += 1) {
145
+ const bucket = grid.get(keyOf(ix, iz));
146
+ if (bucket === undefined)
147
+ continue;
148
+ for (const entry of bucket) {
149
+ if (entry.stamp === stamp)
150
+ continue;
151
+ entry.stamp = stamp;
152
+ if (overlaps(entry, min, max))
153
+ out.push(entry.solid);
154
+ }
155
+ }
156
+ }
157
+ for (const entry of oversize)
158
+ if (overlaps(entry, min, max))
159
+ out.push(entry.solid);
160
+ return out;
161
+ },
162
+ all: () => [...layerMap.values()].flatMap((entries) => entries.map((entry) => entry.solid)),
163
+ count: () => total,
164
+ version: () => revision,
165
+ subscribe(listener) {
166
+ listeners.add(listener);
167
+ return () => {
168
+ listeners.delete(listener);
169
+ };
170
+ },
171
+ snapshot() {
172
+ const layers = {};
173
+ for (const [layer, entries] of layerMap)
174
+ layers[layer] = entries.map((entry) => entry.solid);
175
+ return { layers };
176
+ },
177
+ restore(state) {
178
+ layerMap.clear();
179
+ for (const [layer, solids] of Object.entries(state.layers)) {
180
+ if (solids.length > 0)
181
+ layerMap.set(layer, solids.map(indexed));
182
+ }
183
+ changed();
184
+ },
185
+ };
186
+ }
187
+ /**
188
+ * One solid per generated building: its `bounds` footprint turned by `rotationY` about `center`,
189
+ * from the ground under its center up through its floors. `groundHeight` defaults to flat `0`.
190
+ * @capability world-solids-from-data derive collision boxes from generated buildings and wall runs
191
+ */
192
+ export function buildingSolids(buildings, groundHeight = () => 0) {
193
+ return buildings.map((building) => {
194
+ const { bounds, center } = building;
195
+ const yaw = building.rotationY ?? 0;
196
+ const localX = (bounds.minX + bounds.maxX) / 2 - center[0];
197
+ const localZ = (bounds.minZ + bounds.maxZ) / 2 - center[1];
198
+ const cos = Math.cos(yaw);
199
+ const sin = Math.sin(yaw);
200
+ const base = groundHeight(center[0], center[1]);
201
+ const halfHeight = (building.floors * building.floorHeight) / 2;
202
+ return {
203
+ center: [center[0] + localX * cos + localZ * sin, base + halfHeight, center[1] - localX * sin + localZ * cos],
204
+ halfExtents: [(bounds.maxX - bounds.minX) / 2, halfHeight, (bounds.maxZ - bounds.minZ) / 2],
205
+ ...(yaw === 0 ? {} : { rotationY: yaw }),
206
+ };
207
+ });
208
+ }
209
+ /**
210
+ * Solids for every `structures` descriptor on an environment feature, skipping any marked `solid: false`.
211
+ * @capability world-solids-from-data derive collision boxes from generated buildings and wall runs
212
+ */
213
+ export function structureSolids(world, groundHeight) {
214
+ const solids = [];
215
+ for (const descriptor of world.structures ?? []) {
216
+ if (descriptor.solid === false)
217
+ continue;
218
+ solids.push(...buildingSolids(resolveStructureBuildings(descriptor), groundHeight));
219
+ }
220
+ return solids;
221
+ }
222
+ /**
223
+ * One oriented solid per wall segment (see `wallSegments` in `world/walls`).
224
+ * @capability world-solids-from-data derive collision boxes from generated buildings and wall runs
225
+ */
226
+ export function wallSolids(segments, options) {
227
+ return segments
228
+ .filter((segment) => segment.length > 0)
229
+ .map((segment) => {
230
+ const mid = [(segment.from[0] + segment.to[0]) / 2, (segment.from[1] + segment.to[1]) / 2];
231
+ const base = options.groundHeight?.(mid[0], mid[1]) ?? 0;
232
+ // Local x runs along the segment: rotationY maps local +x to (cos, -sin) in XZ.
233
+ const yaw = -Math.atan2(segment.to[1] - segment.from[1], segment.to[0] - segment.from[0]);
234
+ return {
235
+ center: [mid[0], base + options.height / 2, mid[1]],
236
+ halfExtents: [segment.length / 2, options.height / 2, options.thickness / 2],
237
+ rotationY: yaw,
238
+ };
239
+ });
240
+ }
package/dist/world.d.ts CHANGED
@@ -32,7 +32,7 @@ export { steerYaw } from "./movement/steering.js";
32
32
  export { constrainToNavGrid } from "./nav/navConstrain.js";
33
33
  export { populateNavGridFromEnvironment } from "./nav/navFromEnvironment.js";
34
34
  export { createNavGrid, findPath, slopeStepCost, type NavGrid, type NavPoint } from "./nav/navGrid.js";
35
- export { buildNavAdjacency, closestPoint, raycastNav, type NavMeshAdjacency, type NavMeshData, type NavMeshLink, type NavMeshPath, } from "./nav/navMesh.js";
35
+ export { buildNavAdjacency, closestPoint, createNavMeshQuery, raycastNav, type NavMeshAdjacency, type NavMeshData, type NavMeshLink, type NavMeshPath, type NavMeshQuery, type NavMeshQueryOptions, type NavMeshQuerySnapshot, } from "./nav/navMesh.js";
36
36
  export { advancePathFollow, createPathFollow, pathFollowProgress, pathFollowSeek, pathFromNav, pathLength, type PathFollowConfig, type PathFollowProgress, type PathFollowState, type PathProgress, type Waypoint, } from "./nav/pathFollow.js";
37
37
  export { defineAttackMoveOrder, defineHoldOrder, defineMoveOrder, definePatrolOrder, defineStopOrder, defineTargetedOrder, type AttackMoveOrderPayload, type EmptyOrderPayload, type EngagementKindConfig, type EngagementOrderState, type MoveOrderPayload, type OrderKindConfig, type OrderMoveResult, type OrderMover, type OrderTargeting, type PatrolOrderPayload, type PatrolOrderState, type TargetedOrderPayload, } from "./orders/orderKinds.js";
38
38
  export { createOrderQueue, createOrderRegistry, type Order, type OrderCancelReason, type OrderEvent, type OrderIssueResult, type OrderKind, type OrderOutcome, type OrderPhase, type OrderProgress, type OrderQueue, type OrderQueueOptions, type OrderQueuePolicy, type OrderQueueState, type OrderRegistry, type OrderRejection, type OrderRequest, type OrderStartResult, type OrderTickReport, type OrderVec3, } from "./orders/orderQueue.js";
@@ -58,7 +58,7 @@ export { partsBounds, registerAssetGenerator, type GeneratedAsset, type Generate
58
58
  export { applyRotationPolicy, headingToRotationY, normalizeDegrees, parseAssetSpace, parseRotationPolicy, resolveAnchorOffset, resolveFacingRotationY, rotatedFootprint, rotationYToHeading, snapHeading, toEngineUnits, validateAssetSpace, type AssetAnchor, type AssetSpace, type AssetSpaceIssue, type PlacementRotationPolicy, } from "./scene/assetSpace.js";
59
59
  export { collectAuthoredTriggers, createAuthoredTriggerRuntime, getTriggerAction, listTriggerActions, pointInVolume, registerTriggerAction, type AuthoredTrigger, type AuthoredTriggerRuntime, type TriggerActionDefinition, type TriggerDispatchEvent, type TriggerEvent, type TriggerHandlers, type TriggerSourceKind, } from "./scene/authoredTriggers.js";
60
60
  export { selectAutoTarget, type AutoTargetPolicy } from "./scene/autoTarget.js";
61
- export { advanceBehaviors, behaviorControl, registerBehaviorActions, type BehaviorAction, type BehaviorActionContext, type BehaviorControl, type BehaviorInspection, type BehaviorResumePolicy, type BehaviorSnapshot, type BehaviorStatus, } from "./scene/behaviorRuntime.js";
61
+ export { advanceBehaviors, behaviorControl, registerBehaviorActions, type BehaviorAction, type BehaviorActionContext, type BehaviorActionOptions, type BehaviorControl, type BehaviorInspection, type BehaviorResumePolicy, type BehaviorSnapshot, type BehaviorStatus, } from "./scene/behaviorRuntime.js";
62
62
  export { patrol, player, talkable, wander, type BehaviorDescriptor, type DecisionGraphBehavior, } from "./scene/behaviors.js";
63
63
  export { createBodyBind } from "./scene/bodyBind.js";
64
64
  export { type ColliderPurpose, type EntityColliderSet, type ResolvedCollider } from "./scene/colliders.js";
package/dist/world.js CHANGED
@@ -32,7 +32,7 @@ export { steerYaw } from "./movement/steering.js";
32
32
  export { constrainToNavGrid } from "./nav/navConstrain.js";
33
33
  export { populateNavGridFromEnvironment } from "./nav/navFromEnvironment.js";
34
34
  export { createNavGrid, findPath, slopeStepCost } from "./nav/navGrid.js";
35
- export { buildNavAdjacency, closestPoint, raycastNav, } from "./nav/navMesh.js";
35
+ export { buildNavAdjacency, closestPoint, createNavMeshQuery, raycastNav, } from "./nav/navMesh.js";
36
36
  export { advancePathFollow, createPathFollow, pathFollowProgress, pathFollowSeek, pathFromNav, pathLength, } from "./nav/pathFollow.js";
37
37
  export { defineAttackMoveOrder, defineHoldOrder, defineMoveOrder, definePatrolOrder, defineStopOrder, defineTargetedOrder, } from "./orders/orderKinds.js";
38
38
  export { createOrderQueue, createOrderRegistry, } from "./orders/orderQueue.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/core",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "description": "jgengine core — pure-TypeScript game framework SDK (npm @jgengine/core). Entity stores, commands, combat, inventory, multiplayer seams. Zero dependencies. Not an ECS. Agent skills ship in the tarball. https://jgengine.com",
5
5
  "keywords": [
6
6
  "jgengine",
@@ -143,7 +143,7 @@ When sufficient, return a concise orientation: objective, progress, decisions, c
143
143
 
144
144
  ## JGengine notes
145
145
 
146
- - Multi-session engine work often spans `docs/plans/`, GitHub issues, and stacked PRs — point at those; do not paste full plan bodies.
146
+ - Multi-session engine work spans GitHub issues and PRs — point at those; do not paste full plan bodies.
147
147
  - Prefer `bun run agent:bootstrap --check` status, branch names, and PR links as state anchors.
148
148
  - Local parallelism uses `.claude/worktrees/`; never invent `C:\tmp\…` Codex-shared trees.
149
149
  - Durable process changes belong in skills, not only the handoff.
@@ -33,8 +33,8 @@ An audit is not completion for a build/improve request. Finish when the slice is
33
33
 
34
34
  Before content, write a first-hour balance sheet: starting cash, time to first income, net income per hour at starting hardware and at ten times that hardware, bill cadence versus income, and the insolvency recovery path. Insolvency resets the affected player or changes their options; it never deletes the world. Express clicks and event rewards as a fraction of the designed income rate, never an unrelated flat payout.
35
35
 
36
- Declare currency `decimals` for income paid per second. Anchor time-based decay to `game.createdAt` or a persisted per-instance creation time, never a fixed wall-clock epoch. Test fractional income over real tick cadences before tuning prices.
36
+ Currency precision, elapsed-time anchoring, and reset mechanics are implemented per `jgengine-gameplay` "Persistent economies and input"; test fractional income over real tick cadences before tuning prices.
37
37
 
38
38
  The first-60-seconds gate requires the first tutorial verb to be executable from spawn with starting inventory. Grant dependencies such as land, power, and slots or let players buy them in-context; the tutorial cannot lock the surface needed to complete itself. Placement previews show cost and affordability inline.
39
39
 
40
- For a persistent shared-world builder, compose neighborhood presence, declared command read scopes, membership storage, online-player batched ticks, territory, chat, and join retry UI through their engine owners. Verify capacity and first-player onboarding before scaling the content catalog.
40
+ For a persistent shared-world builder, verify capacity and first-player onboarding before scaling the content catalog; presence, command read scopes, territory, chat, and join retry UI come from their owners in `jgengine-world`, `jgengine-ui`, and `jgengine-multiplayer`.
@@ -5,7 +5,9 @@ description: Route game work to the smallest useful JGengine guidance set.
5
5
 
6
6
  # JGengine intake and router
7
7
 
8
- Read the JGengine README for package truth, commands, layering, and license — the repo-root [`README.md`](../../../README.md) in the monorepo, or [jgengine.com](https://jgengine.com) from a scaffolded consumer project (where the relative link does not resolve). This skill decides what to load; it is not an engine manual.
8
+ This skill decides what to load; it is not an engine manual. Package map, commands, layering, and license live in the JGengine README (repo-root [`README.md`](../../../README.md) in the monorepo, [jgengine.com](https://jgengine.com) from a created project); open it when you need one of those facts.
9
+
10
+ A created project installs only `jgengine`, `jgengine-editor`, `jgengine-verify`, `game-design`, and `jgengine-ui`. Run `npx jgengine skills -p --all` before loading any other domain named below. Those projects do not ship `api.md`; read exact signatures from the `.d.ts` files under `node_modules/@jgengine/<pkg>/dist/`.
9
11
 
10
12
  ## 1. Establish the target
11
13
 
@@ -20,7 +22,7 @@ Treat the pitch as a unique composition of needs, not a genre to fill in — nev
20
22
  - HUD/menu states and **custom** UI art direction (compose shipped building blocks + HudTheme; no unskinned stock face)
21
23
  - one observable completion scenario
22
24
 
23
- For a new in-repo game, scaffold with `bun run new:game "<id-or-title>"` (thin script alias for `jgengine create`, auto-detects the in-repo variant). Outside this monorepo, start with `npx jgengine create`; never copy `Games/*`.
25
+ Pick the dimension first: `create` defaults to a 3D world (terrain, sky, player model, editor scene); a grid, card, tile, or other board game with no 3D scene takes `create --2d` (a `board` world drawn by GameUI over `@jgengine/core/puzzle/cellGrid`). For a new in-repo game, scaffold with `bun run new:game "<id-or-title>"` (thin script alias for `jgengine create`, auto-detects the in-repo variant). Outside this monorepo, start with `npx jgengine create`; never copy `Games/*`.
24
26
 
25
27
  For an existing game asked to pick up a new engine release ("jgengine published a new version, grab it"), run `npx jgengine upgrade` in the project first: it diffs installed `@jgengine/*` versions against the latest release and prints every Migrate step and Adopt-worthy addition in between (`--json` for structured output). Bump, migrate oldest-first, then work the Adopt lists instead of reconstructing the changelog by hand.
26
28
 
@@ -46,9 +48,9 @@ World content routes editor-first: any request that adds, moves, restyles, or re
46
48
 
47
49
  Within each selected domain:
48
50
 
49
- 0. Fastest start for a common composition: `npx jgengine recipe <name>` prints a vetted, SDK-typechecked snippet (imports + wired code) for `combat-loop`, `boss-telegraph`, `loot`, `quest`, `coop-presence`, `third-person-camera`; `npx jgengine recipe` lists them. Copy that instead of reconning per-file.
50
- 1. Search `capabilities.md` by intent; it maps needs to imports. Outside the monorepo (or before a domain skill is even loaded), `npx jgengine find <intent>` greps every shipped capability index at once and prints the primitive + import — reach for it the moment you're tempted to hand-roll a HUD window, inventory grid, paperdoll, hotkey listener, minimap, stat bar, or character motion. If you're writing a `z-index`, a `keydown` for a panel, a `<div>` inventory, or a limb bob, a shipped drop-in almost certainly exists (`usePanels`/`PanelHost`, `InventoryGrid`, `CharacterSheet`/`Paperdoll`, `PartMotionRig`, `EntityPreview`); games own the *skin*, not the re-derivation.
51
- 2. Open `api.md` only when exact signatures or export inventory are needed.
51
+ 0. Fastest start for a common composition: `npx jgengine recipe <name>` prints a vetted, SDK-typechecked snippet (imports + wired code) (`combat-loop`, `loot`, `world-drops`, `click-target`, `ability-bar`, `hitscan-weapon`, `rolled-gear`, `enter-vehicle` and more); `npx jgengine recipe` lists them. Copy that instead of reconning per-file.
52
+ 1. Search capabilities by intent; do not read `capabilities.md` top to bottom. `npx jgengine find <intent>` searches every domain's index and every recipe at once, installed or not, and prints the primitive and its import (or the recipe to open); in the monorepo, `grep -i "<intent>" .claude/skills/*/capabilities.md` does the same. Search the moment you are tempted to hand-roll a HUD window, inventory grid, paperdoll, hotkey listener, minimap, stat bar, or character motion. A `z-index`, a `keydown` for a panel, a `<div>` inventory, or a limb bob almost certainly has a shipped drop-in (`usePanels`/`PanelHost`, `InventoryGrid`, `CharacterSheet`/`Paperdoll`, `PartMotionRig`, `EntityPreview`); games own the *skin*, not the re-derivation.
53
+ 2. Open `api.md` (monorepo) or the package `.d.ts` only when exact signatures are needed; grep it, never read it whole.
52
54
  3. Open the linked reference only for a deeper recipe or trap.
53
55
  4. For how several primitives wire into a running loop, read the domain's `recipes/` — connected, genre-free walkthroughs organized by composition seam; this skill's own [recipes/minimal-game.md](recipes/minimal-game.md) is the default whole-game walkthrough. Prefer a recipe over reading a game's source; the games under `Games/*` are not templates or references, and a hybrid game is just a different composition of the same primitives.
54
56