@jgengine/core 0.11.0 → 0.13.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 (256) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/ai/populationDirector.d.ts +103 -0
  3. package/dist/ai/populationDirector.js +190 -0
  4. package/dist/ai/pursuit.d.ts +56 -0
  5. package/dist/ai/pursuit.js +53 -0
  6. package/dist/authoring.d.ts +1 -1
  7. package/dist/authoring.js +1 -1
  8. package/dist/combat/effects.d.ts +11 -4
  9. package/dist/combat/effects.js +24 -17
  10. package/dist/combat/magazine.d.ts +13 -0
  11. package/dist/combat/magazine.js +27 -0
  12. package/dist/combat/vfxPresets.d.ts +176 -0
  13. package/dist/combat/vfxPresets.js +74 -0
  14. package/dist/combat/weaponFire.d.ts +114 -0
  15. package/dist/combat/weaponFire.js +96 -0
  16. package/dist/combat.d.ts +5 -2
  17. package/dist/combat.js +4 -1
  18. package/dist/devtools/controls.d.ts +38 -0
  19. package/dist/devtools/controls.js +181 -0
  20. package/dist/devtools/devtools.d.ts +5 -197
  21. package/dist/devtools/devtools.js +38 -774
  22. package/dist/devtools/discover.d.ts +32 -0
  23. package/dist/devtools/discover.js +240 -0
  24. package/dist/devtools/fallbackSeams.d.ts +50 -0
  25. package/dist/devtools/fallbackSeams.js +111 -0
  26. package/dist/devtools/frame.d.ts +15 -0
  27. package/dist/devtools/frame.js +247 -0
  28. package/dist/devtools/logs.d.ts +15 -0
  29. package/dist/devtools/logs.js +86 -0
  30. package/dist/devtools/overrides.d.ts +16 -0
  31. package/dist/devtools/overrides.js +66 -0
  32. package/dist/devtools/types.d.ts +212 -0
  33. package/dist/devtools/types.js +1 -0
  34. package/dist/devtools/urlFlags.d.ts +22 -0
  35. package/dist/devtools/urlFlags.js +62 -0
  36. package/dist/economy/auctionBook.d.ts +122 -0
  37. package/dist/economy/auctionBook.js +213 -0
  38. package/dist/economy/priceHistory.d.ts +44 -0
  39. package/dist/economy/priceHistory.js +89 -0
  40. package/dist/economy/unlockPoints.d.ts +92 -0
  41. package/dist/economy/unlockPoints.js +103 -0
  42. package/dist/editor/commandApply.d.ts +5 -0
  43. package/dist/editor/commandApply.js +755 -0
  44. package/dist/editor/commands.d.ts +48 -3
  45. package/dist/editor/commands.js +40 -754
  46. package/dist/editor/directives.d.ts +94 -0
  47. package/dist/editor/directives.js +206 -0
  48. package/dist/editor/document.d.ts +37 -2
  49. package/dist/editor/document.js +422 -6
  50. package/dist/editor/entityCatalog.d.ts +42 -0
  51. package/dist/editor/entityCatalog.js +101 -0
  52. package/dist/editor/environment.d.ts +74 -0
  53. package/dist/editor/environment.js +169 -0
  54. package/dist/editor/index.d.ts +26 -1
  55. package/dist/editor/index.js +8 -1
  56. package/dist/editor/minimap.d.ts +31 -0
  57. package/dist/editor/minimap.js +73 -0
  58. package/dist/editor/streamer.d.ts +66 -0
  59. package/dist/editor/streamer.js +68 -0
  60. package/dist/editor/types.d.ts +143 -0
  61. package/dist/editor/world.d.ts +136 -0
  62. package/dist/editor/world.js +303 -0
  63. package/dist/game/breeding.d.ts +161 -0
  64. package/dist/game/breeding.js +181 -0
  65. package/dist/game/cameraConfig.d.ts +451 -0
  66. package/dist/game/cameraConfig.js +1 -0
  67. package/dist/game/clipRoles.d.ts +51 -0
  68. package/dist/game/clipRoles.js +142 -0
  69. package/dist/game/defineGame.d.ts +16 -9
  70. package/dist/game/defineGame.js +14 -6
  71. package/dist/game/defineSystem.d.ts +2 -0
  72. package/dist/game/defineSystem.js +2 -0
  73. package/dist/game/events.d.ts +28 -0
  74. package/dist/game/partAnimation.d.ts +78 -0
  75. package/dist/game/partAnimation.js +147 -0
  76. package/dist/game/playableGame.d.ts +72 -440
  77. package/dist/game/playableGame.js +1 -1
  78. package/dist/game/progression.d.ts +35 -0
  79. package/dist/game/progression.js +22 -1
  80. package/dist/game/quest.js +1 -3
  81. package/dist/game/tribe.d.ts +184 -0
  82. package/dist/game/tribe.js +432 -0
  83. package/dist/gameplay.d.ts +3 -1
  84. package/dist/gameplay.js +3 -1
  85. package/dist/inventory/encumbrance.d.ts +69 -0
  86. package/dist/inventory/encumbrance.js +68 -0
  87. package/dist/inventory/inventoryModel.d.ts +24 -8
  88. package/dist/inventory/inventoryModel.js +47 -8
  89. package/dist/item/durability.js +1 -3
  90. package/dist/meta/changelog.d.ts +1 -1
  91. package/dist/meta/changelog.js +48 -1
  92. package/dist/movement/movementModel.d.ts +15 -1
  93. package/dist/movement/movementModel.js +33 -15
  94. package/dist/movement/playerMovement.js +102 -2
  95. package/dist/multiplayer/combatSnapshot.js +13 -8
  96. package/dist/multiplayer/presenceModel.d.ts +2 -0
  97. package/dist/multiplayer/presenceModel.js +8 -0
  98. package/dist/physics/drivableVehicle.d.ts +2 -0
  99. package/dist/physics/drivableVehicle.js +1 -1
  100. package/dist/physics/flightDynamics.d.ts +82 -0
  101. package/dist/physics/flightDynamics.js +165 -0
  102. package/dist/physics/gravityField.d.ts +25 -0
  103. package/dist/physics/gravityField.js +41 -0
  104. package/dist/physics/kinematicVehicle.d.ts +87 -0
  105. package/dist/physics/kinematicVehicle.js +193 -11
  106. package/dist/physics/physicsWorld.d.ts +26 -0
  107. package/dist/physics/physicsWorld.js +109 -117
  108. package/dist/physics/vehicleObstacles.d.ts +44 -0
  109. package/dist/physics/vehicleObstacles.js +115 -0
  110. package/dist/random/rng.d.ts +4 -1
  111. package/dist/random/rng.js +4 -1
  112. package/dist/runtime/adapter.d.ts +13 -0
  113. package/dist/runtime/adapter.js +13 -0
  114. package/dist/runtime/context/combat.d.ts +51 -0
  115. package/dist/runtime/context/combat.js +131 -0
  116. package/dist/runtime/context/combatFx.js +15 -5
  117. package/dist/runtime/context/deathLoot.d.ts +30 -0
  118. package/dist/runtime/context/deathLoot.js +39 -0
  119. package/dist/runtime/context/player.d.ts +48 -0
  120. package/dist/runtime/context/player.js +125 -0
  121. package/dist/runtime/context/scene.d.ts +60 -0
  122. package/dist/runtime/context/scene.js +329 -0
  123. package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
  124. package/dist/runtime/context/snapshotCodecs.js +26 -0
  125. package/dist/runtime/descriptors/baseline.js +26 -3
  126. package/dist/runtime/descriptors/features.d.ts +5 -0
  127. package/dist/runtime/descriptors/features.js +20 -2
  128. package/dist/runtime/gameContext.d.ts +2 -428
  129. package/dist/runtime/gameContext.js +133 -500
  130. package/dist/runtime/gameContextTypes.d.ts +500 -0
  131. package/dist/runtime/gameContextTypes.js +1 -0
  132. package/dist/runtime/headlessRunner.d.ts +17 -2
  133. package/dist/runtime/headlessRunner.js +15 -0
  134. package/dist/runtime/hostPolicy.d.ts +51 -0
  135. package/dist/runtime/hostPolicy.js +68 -0
  136. package/dist/runtime/hostedGameRunner.d.ts +3 -1
  137. package/dist/runtime/hostedGameRunner.js +1 -0
  138. package/dist/runtime/hostedWorldSession.d.ts +3 -1
  139. package/dist/runtime/hostedWorldSession.js +1 -0
  140. package/dist/runtime/worldSnapshot.d.ts +13 -1
  141. package/dist/runtime/worldSnapshot.js +15 -3
  142. package/dist/scene/assetCatalog.d.ts +7 -0
  143. package/dist/scene/assetSpace.js +1 -3
  144. package/dist/scene/authoredTriggers.d.ts +50 -0
  145. package/dist/scene/authoredTriggers.js +114 -0
  146. package/dist/scene/builtinSceneKinds.js +8 -3
  147. package/dist/scene/colliders.d.ts +79 -0
  148. package/dist/scene/colliders.js +170 -0
  149. package/dist/scene/collisionMesh.d.ts +94 -0
  150. package/dist/scene/collisionMesh.js +699 -0
  151. package/dist/scene/companion.d.ts +147 -0
  152. package/dist/scene/companion.js +193 -0
  153. package/dist/scene/entityStats.d.ts +7 -9
  154. package/dist/scene/entityStats.js +19 -15
  155. package/dist/scene/sceneKinds.d.ts +16 -0
  156. package/dist/scene/sceneKinds.js +4 -0
  157. package/dist/scene/sceneRaycast.js +15 -0
  158. package/dist/stats/spawnLevelStats.d.ts +110 -0
  159. package/dist/stats/spawnLevelStats.js +140 -0
  160. package/dist/stats/statModifiers.d.ts +16 -0
  161. package/dist/stats/statModifiers.js +30 -1
  162. package/dist/stats/statPool.d.ts +70 -0
  163. package/dist/stats/statPool.js +62 -0
  164. package/dist/store/changeSignal.d.ts +3 -0
  165. package/dist/store/changeSignal.js +2 -0
  166. package/dist/store/observableKeyedStore.d.ts +2 -0
  167. package/dist/store/observableKeyedStore.js +1 -0
  168. package/dist/survival/decayMeter.js +1 -3
  169. package/dist/survival/regionHealth.js +1 -3
  170. package/dist/ui/hudLayout.d.ts +5 -0
  171. package/dist/ui/hudLayout.js +17 -1
  172. package/dist/ui/panelModel.d.ts +120 -0
  173. package/dist/ui/panelModel.js +168 -0
  174. package/dist/ui.d.ts +1 -0
  175. package/dist/ui.js +1 -0
  176. package/dist/world/authoredEntities.d.ts +29 -0
  177. package/dist/world/authoredEntities.js +29 -0
  178. package/dist/world/authoredObjects.d.ts +15 -1
  179. package/dist/world/authoredObjects.js +13 -1
  180. package/dist/world/authoredSpawn.d.ts +39 -0
  181. package/dist/world/authoredSpawn.js +37 -0
  182. package/dist/world/buildSockets.d.ts +152 -0
  183. package/dist/world/buildSockets.js +188 -0
  184. package/dist/world/buildingLots.d.ts +84 -0
  185. package/dist/world/buildingLots.js +119 -0
  186. package/dist/world/buildings.d.ts +6 -0
  187. package/dist/world/cityBlocks.d.ts +152 -0
  188. package/dist/world/cityBlocks.js +670 -0
  189. package/dist/world/cityContent.d.ts +77 -0
  190. package/dist/world/cityContent.js +261 -0
  191. package/dist/world/cityGenerator.d.ts +41 -0
  192. package/dist/world/cityGenerator.js +58 -0
  193. package/dist/world/cityGeometry.d.ts +114 -0
  194. package/dist/world/cityGeometry.js +573 -0
  195. package/dist/world/cityKind.d.ts +314 -0
  196. package/dist/world/cityKind.js +1578 -0
  197. package/dist/world/environmentSummary.js +57 -1
  198. package/dist/world/features.d.ts +53 -5
  199. package/dist/world/features.js +15 -3
  200. package/dist/world/grassKind.js +2 -2
  201. package/dist/world/markers.d.ts +50 -4
  202. package/dist/world/markers.js +49 -0
  203. package/dist/world/massing.js +1 -1
  204. package/dist/world/minimapBake.d.ts +78 -0
  205. package/dist/world/minimapBake.js +208 -0
  206. package/dist/world/minimapTrack.d.ts +17 -0
  207. package/dist/world/minimapTrack.js +22 -0
  208. package/dist/world/place.d.ts +166 -0
  209. package/dist/world/place.js +96 -0
  210. package/dist/world/placeAsset.js +3 -1
  211. package/dist/world/poleLineKind.d.ts +40 -0
  212. package/dist/world/poleLineKind.js +97 -0
  213. package/dist/world/polyline.js +9 -2
  214. package/dist/world/resourceNode.d.ts +109 -0
  215. package/dist/world/resourceNode.js +140 -0
  216. package/dist/world/roads.d.ts +19 -2
  217. package/dist/world/roads.js +60 -3
  218. package/dist/world/scatter.js +2 -20
  219. package/dist/world/streetGenerator.d.ts +159 -0
  220. package/dist/world/streetGenerator.js +684 -0
  221. package/dist/world/streets.d.ts +103 -1
  222. package/dist/world/streets.js +273 -0
  223. package/dist/world/terraform.js +58 -88
  224. package/dist/world/terrain.d.ts +65 -1
  225. package/dist/world/terrain.js +119 -2
  226. package/dist/world.d.ts +14 -5
  227. package/dist/world.js +12 -3
  228. package/package.json +1 -1
  229. package/skills/game-design/SKILL.md +28 -0
  230. package/skills/game-design/agents/openai.yaml +4 -0
  231. package/skills/game-design/references/application-playbook.md +170 -0
  232. package/skills/game-design/references/field-guide.md +288 -0
  233. package/skills/jgengine/SKILL.md +8 -4
  234. package/skills/jgengine/api.md +182 -81
  235. package/skills/jgengine/capabilities.md +104 -2
  236. package/skills/jgengine/recipes/minimal-game.md +88 -0
  237. package/skills/jgengine-combat/SKILL.md +19 -1
  238. package/skills/jgengine-combat/api.md +75 -2
  239. package/skills/jgengine-combat/capabilities.md +61 -1
  240. package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
  241. package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
  242. package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
  243. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
  244. package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
  245. package/skills/jgengine-combat/reference.md +33 -4
  246. package/skills/jgengine-gameplay/SKILL.md +8 -0
  247. package/skills/jgengine-gameplay/api.md +182 -26
  248. package/skills/jgengine-gameplay/capabilities.md +96 -2
  249. package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
  250. package/skills/jgengine-gameplay/reference-systems.md +10 -5
  251. package/skills/jgengine-world/SKILL.md +13 -0
  252. package/skills/jgengine-world/api.md +453 -21
  253. package/skills/jgengine-world/capabilities.md +112 -2
  254. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  255. package/skills/jgengine-world/reference.md +43 -4
  256. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -63,16 +63,30 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
63
63
 
64
64
  - `defineAttackMoveOrder` (function) · `import { defineAttackMoveOrder } from "@jgengine/core/world"`
65
65
 
66
- ## authored-objects — place catalog mesh props from an editor document
66
+ ## authored-entities — spawn authored mob/boss markers from the editor document at runtime
67
67
 
68
+ - `authoredEntitySpawns` (function) · `import { authoredEntitySpawns } from "@jgengine/core/world/authoredEntities"`
69
+
70
+ ## authored-objects — marker kinds resolved as entity spawns, not static props
71
+
72
+ - `ENTITY_MARKER_KINDS` (const) · `import { ENTITY_MARKER_KINDS } from "@jgengine/core/world/authoredObjects"`
68
73
  - `markerCatalogId` (function) · `import { markerCatalogId } from "@jgengine/core/world"`
69
74
  - `placeAuthoredObjects` (function) · `import { placeAuthoredObjects } from "@jgengine/core/world"`
70
75
  - `resolveAuthoredObjects` (function) · `import { resolveAuthoredObjects } from "@jgengine/core/world"`
71
76
 
72
- ## authored-triggers — schema'd on/action vocabulary on volumes and markers with runtime dispatch
77
+ ## authored-spawn — read spawn points and markers from the editor document
78
+
79
+ - `authoredSpawnPosition` (function) · `import { authoredSpawnPosition } from "@jgengine/core/world/authoredSpawn"`
80
+ - `authoredSpawnRotation` (function) · `import { authoredSpawnRotation } from "@jgengine/core/world/authoredSpawn"`
81
+ - `markersOfKind` (function) · `import { markersOfKind } from "@jgengine/core/world/authoredSpawn"`
82
+
83
+ ## authored-triggers — built-in announce/win/advance actions authorable on any marker or volume
73
84
 
85
+ - `BUILTIN_TRIGGER_ACTIONS` (const) · `import { BUILTIN_TRIGGER_ACTIONS } from "@jgengine/core/scene/authoredTriggers"`
74
86
  - `collectAuthoredTriggers` (function) · `import { collectAuthoredTriggers } from "@jgengine/core/world"`
75
87
  - `createAuthoredTriggerRuntime` (function) · `import { createAuthoredTriggerRuntime } from "@jgengine/core/world"`
88
+ - `createTriggerOutcome` (function) · `import { createTriggerOutcome } from "@jgengine/core/scene/authoredTriggers"`
89
+ - `registerBuiltinTriggerActions` (function) · `import { registerBuiltinTriggerActions } from "@jgengine/core/scene/authoredTriggers"`
76
90
  - `registerTriggerAction` (function) · `import { registerTriggerAction } from "@jgengine/core/world"`
77
91
 
78
92
  ## behavior-control — pause/disable/resume/seek/inspect behavior instances per entity
@@ -87,6 +101,31 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
87
101
 
88
102
  - `createBodyBind` (function) · `import { createBodyBind } from "@jgengine/core/world"`
89
103
 
104
+ ## build-footprint-cells — grid cells a snapped build piece footprint covers (footprintGrid occupancy bridge)
105
+
106
+ - `footprintCells` (function) · `import { footprintCells } from "@jgengine/core/world/buildSockets"`
107
+
108
+ ## build-socket-catalog — data-driven named-socket snap catalog for modular base building
109
+
110
+ - `createBuildSocketCatalog` (function) · `import { createBuildSocketCatalog } from "@jgengine/core/world/buildSockets"`
111
+
112
+ ## building-lots — derive street-facing building lots from road frontage
113
+
114
+ - `deriveBuildingLots` (function) · `import { deriveBuildingLots } from "@jgengine/core/world/buildingLots"`
115
+
116
+ ## city-district — polygon math for road-derived blocks, parcels, and buildable footprints
117
+
118
+ - `Vec2` (type) · `import { Vec2 } from "@jgengine/core/world/cityGeometry"`
119
+ - `resolveCityObject` (function) · `import { resolveCityObject } from "@jgengine/core/world"`
120
+
121
+ ## companion-intent — resolve a companion's command + threat context into a per-tick follow/hold/engage/assist intent
122
+
123
+ - `resolveCompanionIntent` (function) · `import { resolveCompanionIntent } from "@jgengine/core/scene/companion"`
124
+
125
+ ## companion-roster — manage tamed companions — allegiance, commands, per-companion stats/leveling, and mobBrain-shaped intent
126
+
127
+ - `createCompanionRoster` (function) · `import { createCompanionRoster } from "@jgengine/core/scene/companion"`
128
+
90
129
  ## contour-interval — readable auto-spacing for elevation contours
91
130
 
92
131
  - `chooseContourInterval` (function) · `import { chooseContourInterval } from "@jgengine/core/world"`
@@ -111,6 +150,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
111
150
 
112
151
  - `entityMetaOf` (function) · `import { entityMetaOf } from "@jgengine/core/world"`
113
152
 
153
+ ## flight-dynamics — simulate fixed-wing, helicopter, and VTOL aircraft
154
+
155
+ - `createAircraftDynamics` (function) · `import { createAircraftDynamics } from "@jgengine/core/world"`
156
+
114
157
  ## follow-trail — trailing follower/snake formation that chases a leader
115
158
 
116
159
  - `createLeaderTrail` (function) · `import { createLeaderTrail } from "@jgengine/core/world"`
@@ -159,6 +202,12 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
159
202
 
160
203
  - `createGrappleSwing` (function) · `import { createGrappleSwing } from "@jgengine/core/world"`
161
204
 
205
+ ## gravity-field — sample configurable uniform, radial, or combined gravity
206
+
207
+ - `combineGravity` (function) · `import { combineGravity } from "@jgengine/core/world"`
208
+ - `pointGravity` (function) · `import { pointGravity } from "@jgengine/core/world"`
209
+ - `uniformGravity` (function) · `import { uniformGravity } from "@jgengine/core/world"`
210
+
162
211
  ## hold-order — stand-ground order that holds position until preempted or canceled
163
212
 
164
213
  - `defineHoldOrder` (function) · `import { defineHoldOrder } from "@jgengine/core/world"`
@@ -191,6 +240,23 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
191
240
 
192
241
  - `generateLock` (function) · `import { generateLock } from "@jgengine/core/world"`
193
242
 
243
+ ## mesh-hitboxes — shots pass through holes in concave models — opted-in catalog assets raycast their actual triangles instead of the fitted box.
244
+
245
+ - `ColliderShape` (type) · `import { ColliderShape } from "@jgengine/core/scene/colliders"`
246
+
247
+ ## mesh-movement — the walking player slides against a mesh collider's compound `boxes` — a capsule walks THROUGH an opted-in archway opening and is stopped by its pillars/lintel instead of one outer box.
248
+
249
+ - `ColliderShape` (type) · `import { ColliderShape } from "@jgengine/core/scene/colliders"`
250
+
251
+ ## minimap-bake — rasterize authored terrain + zones into a top-down minimap image
252
+
253
+ - `bakeMinimapImage` (function) · `import { bakeMinimapImage } from "@jgengine/core/world"`
254
+ - `minimapBakeToPngDataUri` (function) · `import { minimapBakeToPngDataUri } from "@jgengine/core/world"`
255
+
256
+ ## minimap-track — project a world point to a 0..1 fraction along a linear track axis
257
+
258
+ - `trackFraction` (function) · `import { trackFraction } from "@jgengine/core/world/minimapTrack"`
259
+
194
260
  ## model-sockets — named attachment points read from a model
195
261
 
196
262
  - `ModelNode` (interface) · `import { ModelNode } from "@jgengine/core/world"`
@@ -228,6 +294,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
228
294
 
229
295
  - `definePatrolOrder` (function) · `import { definePatrolOrder } from "@jgengine/core/world"`
230
296
 
297
+ ## piece-sockets-from-model — derive build-piece snap sockets from a model's named attach points
298
+
299
+ - `pieceSocketsFromModel` (function) · `import { pieceSocketsFromModel } from "@jgengine/core/world/buildSockets"`
300
+
231
301
  ## place-asset — resolve a placement commit into a shared asset placement payload
232
302
 
233
303
  - `PlaceAssetResult` (interface) · `import { PlaceAssetResult } from "@jgengine/core/world"`
@@ -248,10 +318,26 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
248
318
  - `parseRotationPolicy` (function) · `import { parseRotationPolicy } from "@jgengine/core/world"`
249
319
  - `snapHeading` (function) · `import { snapHeading } from "@jgengine/core/world"`
250
320
 
321
+ ## pole-line — editor-authorable poles + sagging cables along a path
322
+
323
+ - `POLE_LINE_KIND` (const) · `import { POLE_LINE_KIND } from "@jgengine/core/world/poleLineKind"`
324
+
325
+ ## population-director — ambient per-region creature population manager with timed weighted respawns and caps
326
+
327
+ - `createPopulationDirector` (function) · `import { createPopulationDirector } from "@jgengine/core/ai/populationDirector"`
328
+
329
+ ## portable-marker-source — project caller-owned arrays or subscribable stores into cached minimap marker snapshots without a MarkerSet
330
+
331
+ - `createMarkerSource` (function) · `import { createMarkerSource } from "@jgengine/core/world"`
332
+
251
333
  ## proximity-prompt — a "press E" contextual prompt shown near an interactable
252
334
 
253
335
  - `resolveActivePrompt` (function) · `import { resolveActivePrompt } from "@jgengine/core/world"`
254
336
 
337
+ ## pursuit — chase-to-reach cooldown-gated attack with optional leash-to-home; owns the attack cooldown
338
+
339
+ - `advancePursuit` (function) · `import { advancePursuit } from "@jgengine/core/ai/pursuit"`
340
+
255
341
  ## qte — a quick-time-event timed input sequence with hit windows
256
342
 
257
343
  - `evaluateQteSequence` (function) · `import { evaluateQteSequence } from "@jgengine/core/world"`
@@ -260,6 +346,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
260
346
 
261
347
  - `tierForStanding` (function) · `import { tierForStanding } from "@jgengine/core/world"`
262
348
 
349
+ ## resource-node — depletable, respawning harvest nodes with tool-dependent yields
350
+
351
+ - `createResourceNodeField` (function) · `import { createResourceNodeField } from "@jgengine/core/world/resourceNode"`
352
+
263
353
  ## rts-control-groups — map Ctrl+digit / digit / double-tap presses onto selection-bookmark bind / recall / focus intents
264
354
 
265
355
  - `controlGroupKey` (function) · `import { controlGroupKey } from "@jgengine/core/world"`
@@ -325,6 +415,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
325
415
 
326
416
  - `defineStopOrder` (function) · `import { defineStopOrder } from "@jgengine/core/world"`
327
417
 
418
+ ## street-generator — build a deterministic street/track graph (nodes, edges, streets, junctions, bridges, tunnels) from sliders
419
+
420
+ - `generateStreets` (function) · `import { generateStreets } from "@jgengine/core/world/streetGenerator"`
421
+
328
422
  ## surface-drape — lift an XZ polyline onto sculpted terrain
329
423
 
330
424
  - `drapePolyline` (function) · `import { drapePolyline } from "@jgengine/core/world"`
@@ -357,6 +451,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
357
451
 
358
452
  - `terrainContourGuides` (function) · `import { terrainContourGuides } from "@jgengine/core/world"`
359
453
 
454
+ ## terrain-raycast — pick a point on a heightfield along a camera/pointer ray
455
+
456
+ - `raycastHeightField` (function) · `import { raycastHeightField } from "@jgengine/core/world/terrain"`
457
+
360
458
  ## touch-controls — default on-screen button silhouette for a touch action
361
459
 
362
460
  - `touchButtonShape` (function) · `import { touchButtonShape } from "@jgengine/core/input/touchScheme"`
@@ -368,3 +466,15 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
368
466
  ## volumetric-clouds — raymarched cloud layer sky option
369
467
 
370
468
  - `VolumetricCloudsConfig` (interface) · `import { VolumetricCloudsConfig } from "@jgengine/core/world"`
469
+
470
+ ## world-environment — compose editor/preset-derived outdoor dressing — terrain, weather, water, structures — into a legacy environment feature
471
+
472
+ - `environment` (function) · `import { environment } from "@jgengine/core/world"`
473
+
474
+ ## world-place — declare the place a game happens in — flat/round/voxel/board ground, surface laws, per-place physics
475
+
476
+ - `world` (function) · `import { world } from "@jgengine/core/world"`
477
+
478
+ ## world-seed — derive a deterministic generator/scatter seed from world id + save/run seed
479
+
480
+ - `seedForPlace` (function) · `import { seedForPlace } from "@jgengine/core/world"`
@@ -0,0 +1,128 @@
1
+ # Recipe — commanding entities (select → order → tick → resolve)
2
+
3
+ **What this wires:** direct command over a group of entities — pick some, issue
4
+ intent, watch them carry it out and interact on arrival. This is a *composition*,
5
+ not a genre. The exact same wiring is the command layer of a base-defense wave, a
6
+ squad-tactics turn, a creature-herding puzzle, or the topdown-survival phase of a
7
+ game that was a card game a minute ago. You swap content, camera, and
8
+ win-condition — never this architecture.
9
+
10
+ It composes four primitives across three seams. The engine never touches your
11
+ entity store directly; you supply narrow adapters, so the same command loop runs
12
+ over any movement/combat model.
13
+
14
+ ## The seams
15
+
16
+ - **Pick → ids.** `createSelectionSet` is an ordered, de-duplicated set of entity
17
+ ids. What *fills* it is up to you — a pointer marquee, a card that names a lane,
18
+ an ability that grabs everything in a radius, an AI planner. Selection is just
19
+ ids; it does not know or care how they were chosen.
20
+ - **Intent → per-entity queue.** Build one shared `createOrderRegistry` and give
21
+ each commandable entity its own `createOrderQueue`. Register verbs as *data*
22
+ (`defineMoveOrder`, `defineAttackMoveOrder`, `defineTargetedOrder`, or your
23
+ own). Every verb composes over two adapters **you** implement against your
24
+ entity store: `OrderMover` (`position` / `moveToward` / `halt`) and
25
+ `OrderTargeting` (`acquire` / `positionOf`). This adapter boundary is what makes
26
+ the loop genre-free.
27
+ - **Resolve → your effect.** In `onTick`, advance each queue. Engagement verbs
28
+ write `{ engaging, inRange }` into `order.state`; you read that flag and resolve
29
+ the actual interaction your way — damage, harvest, repair, heal, dialogue —
30
+ through the combat seam `ctx.scene.entity.effect(...)`. Orders decide *when*;
31
+ the game decides *what*.
32
+
33
+ ## Connected wiring
34
+
35
+ ```ts
36
+ import { createSelectionSet } from "@jgengine/core/scene/selection";
37
+ import {
38
+ createOrderRegistry,
39
+ createOrderQueue,
40
+ defineMoveOrder,
41
+ defineAttackMoveOrder,
42
+ defineTargetedOrder,
43
+ } from "@jgengine/core/world";
44
+
45
+ // --- once, at setup -------------------------------------------------------
46
+
47
+ // Adapters bind orders to YOUR entity store — the only game-specific glue.
48
+ const mover = {
49
+ position: (id) => entities.get(id).pos,
50
+ moveToward: (id, to, dt) => stepToward(entities.get(id), to, dt), // your movement
51
+ halt: (id) => (entities.get(id).vel = [0, 0, 0]),
52
+ };
53
+ const targeting = {
54
+ positionOf: (id) => entities.get(id)?.pos,
55
+ acquire: (id) => nearestEnemyOf(id), // your target pick (kd-tree, grid, faction…)
56
+ };
57
+
58
+ const registry = createOrderRegistry();
59
+ registry.register(defineMoveOrder({ kind: "move", mover }));
60
+ registry.register(defineAttackMoveOrder({ kind: "attack-move", mover, targeting }));
61
+ registry.register(defineTargetedOrder({ kind: "attack", mover, targeting }));
62
+
63
+ const queues = new Map(); // entityId -> OrderQueue
64
+ const queueOf = (id) => {
65
+ let q = queues.get(id);
66
+ if (!q) queues.set(id, (q = createOrderQueue(registry)));
67
+ return q;
68
+ };
69
+
70
+ const selection = createSelectionSet();
71
+
72
+ // --- on a command (pointer marquee, a card play, an ability, an AI plan) --
73
+
74
+ function commandSelection(kind, payload) {
75
+ for (const id of selection) {
76
+ queueOf(id).issue({ kind, payload, policy: "replace" }); // shift-queue => "append"
77
+ }
78
+ }
79
+ // e.g. commandSelection("attack-move", { point: worldHit });
80
+
81
+ // --- every frame ----------------------------------------------------------
82
+
83
+ function onTick(ctx, dt) {
84
+ for (const [id, q] of queues) {
85
+ q.tick(ctx, dt); // activates/advances/completes orders, drives the mover
86
+ const active = q.active;
87
+ if (active?.state?.inRange) {
88
+ // Engagement verb says a target is in range — resolve it the game's way.
89
+ ctx.scene.entity.effect({ from: id, to: active.state.target, effect: "damage" });
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ That is the whole command loop. Everything genre-specific lives in the three
96
+ functions you supplied (`stepToward`, `nearestEnemyOf`, and the `"damage"` effect)
97
+ — never in the order/selection primitives.
98
+
99
+ ## Composing it into different games (and hybrids)
100
+
101
+ The wiring above is fixed; four independent knobs turn it into different play:
102
+
103
+ - **Who fills the selection.** Pointer marquee for an RTS; a targeting card for a
104
+ card-driven skirmish; an AoE ability for a spell; an AI director for autobattle.
105
+ Same `selection`, different caller.
106
+ - **Which camera renders it.** `topDown`, `rts`, `shoulder`, or `first` — camera is
107
+ presentation and reads none of this. Swapping the rig never touches the loop.
108
+ - **What the effect does.** `"damage"` for combat; register a `defineTargetedOrder`
109
+ with a `"harvest"` / `"repair"` / `"talk"` effect for non-combat command. The
110
+ engagement seam is generic; combat is one instance.
111
+ - **When intent arrives.** A **mutating** game reuses one set of queues across
112
+ phases: the card-game phase calls `issue()` from card plays; when it turns into
113
+ a topdown-survival phase, the *same* queues now take `move` / `attack-move` from
114
+ pointer input. You changed who calls `issue()`, not the architecture.
115
+
116
+ ## Where to go next
117
+
118
+ - Marquee/box-select and right-click-to-command come from the shell pointer seam
119
+ (`PlayableGame.pointer`: `select`, `selectFilter`, `orderCommand`) — that is the
120
+ pointer *picker* that fills `selection` and calls `commandSelection`; see
121
+ `jgengine-ui` for the pointer wiring.
122
+ - Pathing for `moveToward` can use `findPath` (`@jgengine/core/nav/navGrid`) over a
123
+ walkable grid; the mover adapter is where it plugs in.
124
+ - Formations, order preemption policies, save/replication of in-flight orders, and
125
+ the full order-kind contract are in [../reference.md](../reference.md) under
126
+ *Entity orders (command queue)*.
127
+ - Damage numbers, death, and rewards are `jgengine-combat`; the effect call above
128
+ is the seam into it.
@@ -2,9 +2,35 @@
2
2
 
3
3
  Reference module for the [`jgengine-world` API](SKILL.md) skill. Load this when you need the renderer-free world surface.
4
4
 
5
- ## World features
5
+ ## World = place (the definition of a world)
6
6
 
7
- Descriptors from `@jgengine/core/world/features` — config data the runner/world layer interprets:
7
+ A world is the place a game happens in — substrate + laws — declared with `world()` from
8
+ `@jgengine/core/world/place`. It is never a coded diorama: sky look, foliage scatter, props, and
9
+ sculpt are scene content authored in the editor (`jgengine-editor`), and the engine renders its
10
+ default sky when the document has none.
11
+
12
+ ```ts
13
+ import { world } from "@jgengine/core/world/place";
14
+
15
+ export const overworld = world({
16
+ id: "overworld", // place identity: saves, seeds, switching
17
+ ground: { mode: "flat", size: { x: Infinity, z: Infinity } },
18
+ physics: { gravity: -24 }, // laws of THIS place
19
+ });
20
+ ```
21
+
22
+ - `ground.mode` discriminates `size`: `flat` takes `{ x, z }` (or `{ x, y, z }`; `Infinity` = unbounded axis), `round` takes `{ radius }` only, `voxel` takes the generator domain `{ x, y, z }` (axes may be `Infinity`), `board` takes `{ x, y }` in cells/layout units (2D surface you look at — grid games, solitaire, tabletop; `stage` is a `board` alias). TS rejects a `radius` on `flat` and `x`/`z` on `round`.
23
+ - `ground.surface` is optional matter/feel law (`{ matter?, friction?, restitution?, traits? }`) systems and audio read — metal vs slime vs felt changes the feel of the *same* rule systems, never forks them. Not a theme kit.
24
+ - `ground.generator` (procedural modes) is algorithm params only — never a seed, never a genre/franchise preset.
25
+ - **No `seed` in a world definition, ever.** The engine derives seeds from world `id` + save/run via `seedForPlace(id, runSeed)` and injects them into generators and scatter.
26
+ - Multiple worlds per game are first-class: one `world()` per place, each with its own ground + physics (`defineGameDefinition` resolves the active place's physics over the game default). Pure UI/rules games omit `world` entirely.
27
+ - Thin defaults: `flat` + infinite axes + default physics for 3D, `board` for 2D, or no world at all. Never scaffold a "meadow with sky preset and seeded grass" — that is editor content.
28
+
29
+ ## Legacy world features
30
+
31
+ Descriptors from `@jgengine/core/world/features` — legacy code-declared world shapes kept for
32
+ existing games. Do not start new worlds here; `environment()` is now the consumption target for
33
+ editor/preset-written scene data (`environmentContentFromDocument`), not a world definition:
8
34
 
9
35
  | Feature | Use |
10
36
  |---------|-----|
@@ -25,6 +51,11 @@ Descriptors from `@jgengine/core/world/features` — config data the runner/worl
25
51
 
26
52
  `parentSpace` positions are local to that space — convert at seams only.
27
53
 
54
+ Display-only map consumers use the portable `MarkerView` / `MarkerSource`
55
+ contracts in `@jgengine/core/world/markers`; the React integration and complete
56
+ external-store walkthrough are owned by the
57
+ [portable minimap recipe](../jgengine-ui/recipes/portable-minimap.md).
58
+
28
59
  ### Query primitives (renderer-free, for gameplay)
29
60
 
30
61
  Pure `@jgengine/core` functions so gameplay reads the same world the shell renders — no three.js needed:
@@ -84,7 +115,7 @@ Turn data-only placement into the build tooling of Valheim/Enshrouded/The Sims/F
84
115
 
85
116
  **Body shape: box or sphere.** `AddBodyOptions` is a discriminated union — `{ shape?: "box", halfExtents }` (box is the default, `shape` omittable) or `{ shape: "sphere", radius }` (the radius fills all three half-extent columns, so broadphase/bounds see the sphere's enclosing AABB). `world.shape[i]` (`SHAPE_BOX` / `SHAPE_SPHERE`) reports a live body's shape for a consumer walking the raw SoA arrays. Sphere-sphere and sphere-box pairs resolve with a proper radial normal (not the axis-aligned box/box path) — balls, projectile bodies, and rolling debris collide correctly against both boxes and each other.
86
117
 
87
- **Collision shapes are box/sphere/voxel only.** Every collider in the engine — `PhysicsWorld` bodies (box `halfExtents` or sphere `radius`), object/entity picking (`scene/objectQuery` raycasts against unit boxes), and `world/voxelField`/`world/carve` blocks — is an axis-aligned box, a sphere, or a voxel cell. Arbitrary authored level-mesh collision (a sculpted GLB as a collider) is not supported. The seams for custom collision are `movement.beforeCommit` (steer or replace the walk controller's resolved step) and object raycasts (query the scene yourself and react) — not a mesh collider.
118
+ **Collision shapes are box/sphere/voxel — plus opt-in triangle meshes for raycasts and mesh-accurate walk obstruction.** Every collider in the engine — `PhysicsWorld` bodies (box `halfExtents` or sphere `radius`), object/entity picking (`scene/objectQuery` raycasts against unit boxes), and `world/voxelField`/`world/carve` blocks — is an axis-aligned box, a sphere, or a voxel cell. The one exception is opted-in catalog assets: an asset listed in `@jgengine/assets` `COLLISION_MESH_ASSET_IDS` ships a compact quantized triangle mesh in the generated index (`scene/collisionMesh` `CollisionMeshData`, extracted from the GLB at reindex — never from the rendered scene, so a headless host resolves the same shape) **plus a deterministic compound-box decomposition** (`voxelizeToBoxes` → `CollisionMeshData.boxes`, a voxel-rasterize + column-fill + greedy-merge of the model's solid volume). Collider auto-fit then (a) raycasts the actual triangles through a precomputed BVH — shots pass through a torus hole or an archway instead of hitting the fitted box; and (b) exposes the compound boxes on the fitted `mesh` `ColliderShape` (`ColliderShape.mesh.boxes`, scaled/translated onto the placement like the mesh). The `collideObjects` walk controller consumes these in `stepPlayerMovement` (host and client alike, so parity is automatic): it gathers obstacles from resolved blocking physical colliders via a bounded `object.inBox` broadphase (reach ≥ radius + max half-extent, so a wide wall's full span blocks, not a phantom 1 m box), carries each collider's real `halfExtents`/`offset` — or, for a mesh collider, the compound sub-boxes — yaw-rotated onto the obstacle, and slides the capsule against **each** sub-box (`resolveObstacleStep`), so a capsule walks THROUGH an archway opening and is stopped by its pillars/lintel/sill. `PhysicsWorld` and overlap tests still see the conservative fitted AABB; objects whose colliders don't block stop obstructing, and non-opted assets keep the fitted box everywhere. Objects with no index `dims` are not stuck with the default unit box either: the shell measures what `renderObject`/dims-less models actually mount and reports it through `ctx.scene.object.reportBounds`, so a custom-rendered prop obstructs (and raycasts) as its real rendered footprint; only objects with nothing measurable keep the default box. The seams for custom *walk* collision remain `movement.beforeCommit` and object raycasts.
88
119
 
89
120
  **Ballistic collision sweep (`physics/ballisticSweep`).** `createBallisticSweep(world, { step?, radius? })` → `BallisticSweep`, a `(origin, velocity, gravity, maxTime) => BallisticSweepHit | null` function that marches the closed-form arc (constant gravity, straight lateral) through a `PhysicsWorld` and reports the first sample inside any live body's AABB (sleeping bodies included), refined by one bisection step; `null` means the whole arc is clear. `step` (default 1/60) is the march interval in seconds, `radius` (default 0) inflates every body's AABB before the point test — pass the projectile's own radius. Wire it into `combat/projectiles` via `ProjectileSystemDeps.sweepBallistic`: when set, a ballistic shot settles at the swept impact point instead of the closed-form landing; omitted or `null` falls back to that closed-form arc.
90
121
 
@@ -160,6 +191,15 @@ runtime.step({
160
191
  // types: TriggerEvent · TriggerActionDefinition · AuthoredTrigger · TriggerHandlers · TriggerSourceKind · AuthoredTriggerRuntime
161
192
  ```
162
193
 
194
+ **Built-in actions** — every game gets `announce` / `win` / `advance` for free, so "enter this zone → win" is authored entirely in the editor with no bespoke action code. `registerBuiltinTriggerActions()` exposes them in the inspector; `createTriggerOutcome()` returns a headless, serializable `TriggerOutcome` (`{ won, message, tone, objective, revision }`) plus the `handlers` to pass the runtime — the HUD reads it via `useSyncExternalStore`. That pair is the whole zero-code "reach the goal to win" wiring the create scaffold ships (a `goal` marker with `{ on: enter, action: win }`).
195
+
196
+ ```ts
197
+ registerBuiltinTriggerActions(); // announce/win/advance in the editor
198
+ const outcome = createTriggerOutcome(); // { get, subscribe, handlers, reset }
199
+ const runtime = createAuthoredTriggerRuntime({ document: editorLayers, handlers: outcome.handlers });
200
+ // HUD: const status = useSyncExternalStore(outcome.subscribe, outcome.get, outcome.get); // status.won / status.message
201
+ ```
202
+
163
203
  Multi-triggers: `meta.triggers: [{ on, action, ...params }]`. Editor inspector renders action params from the declared schema when the game has registered actions.
164
204
 
165
205
  ### Continuous area effects (source-following membership)
@@ -219,4 +259,3 @@ Path progress adapter (`nav/pathFollow`): `PathProgress` is `{ kind: "normalized
219
259
  - **Preemption policy** on each issue: `replace` (preempt active + clear pending), `append` (shift-queue), `front` (jump the queue without dropping the active order), `reject` (refuse while busy). Mark an order `uninterruptible` and a replacing order waits until it finishes; explicit `cancelActive` still forces it.
220
260
  - **Order kinds** are `OrderKind<Ctx, Payload>` with `start`/`update`/`finish`, composed over two narrow adapters — `OrderMover` (`position`/`moveToward`/`halt`) and `OrderTargeting` (`acquire`/`positionOf`). Built-ins ship as ordinary compositions with a configurable `kind` string so a game re-skins a verb ("harvest") without engine edits: `defineMoveOrder`, `defineStopOrder`, `defineHoldOrder`, `defineAttackMoveOrder`, `defineTargetedOrder`, `definePatrolOrder`. Engagement verbs write `{ engaging, inRange }` into `order.state`; the game reads it to run the actual attack/effect, keeping combat resolution game-side.
221
261
 
222
-
@@ -1,72 +0,0 @@
1
- # jgengine-api — Cartridge
2
-
3
- Reference module for the [`jgengine`](./SKILL.md) skill. Load this when building a game as a **cartridge** — one declarative config instead of hand-wired loop/state/UI files. A cartridge game is ~75% smaller than the classic shape and nearly all of it is data; reach for `defineGame` + `defineSystem` ([reference-systems.md](./reference-systems.md)) when the simulation outgrows cartridge archetypes — same single authoring model, no rewrite cliff.
4
-
5
- ## Why
6
-
7
- Across the classic games, 78–91% of each game's LOC was generic wiring restated per game: a run-state store with subscribe/notify, chase-and-contact enemy loops, auto-fire weapon routines, magnet pickups, draft plumbing, phase-gated HUD trees, and tests for all of it. The cartridge layer owns every one of those; the game ships only its **data** (catalogs, tuning, layout) and any genuinely bespoke rules through typed escape hatches.
8
-
9
- ## Shape
10
-
11
- `game.config.ts` is the whole game — `cartridge({...})` from `@jgengine/shell/cartridge` compiles the config into a `PlayableGame` (validating it first — a bad reference throws at import with a list of problems). `loop.ts` and `world.ts` disappear; the spec carries the world feature and the loop is engine-owned. `main.tsx` is 5 lines via `mountGame` (`@/components/ui/mount`, registry). Sprites/SVG art stay in `src/game/assets.ts`; the game keeps one test file under `src/game/`.
12
-
13
- ```ts
14
- import { cartridge, type CartridgeConfig } from "@jgengine/shell/cartridge";
15
- import { standardCartridgePanels } from "@/components/ui/cartridge-panels";
16
-
17
- export const config: CartridgeConfig = {
18
- name, seed, panels: standardCartridgePanels, assets, entitySprites,
19
- flow: { start: "gate", countdownSeconds: 3, restart: true }, // press-to-start → 3-2-1 → playing; "restart" command
20
- player: { kind, health, walkSpeed }, // compiled into content + spawned per player
21
- enemies: { id: { label, health, walkSpeed, xp, contact: { damage, intervalSeconds }, behavior: "chase" | "none" } },
22
- combat: { contactRadius },
23
- spawning: { director: SpawnDirectorConfig, placement: { kind: "ring", radius } },
24
- weapons: { id: { kind: "projectile" | "orbit" | "pulse" | "custom", damage, cooldownMs, maxLevel, ... } },
25
- progression: { xp: Curve, maxLevel, draft: { choices, upgrades } },
26
- fields: { magnetRadius, damageMultiplier }, // named run-scalars upgrades mutate
27
- xpGems: { collectRadius, pullSpeed, rarityThresholds, defaultRarity },
28
- rules: { win: { kind: "survive", seconds }, lose: { kind: "playerDeath" } | { kind: "custom", check }, killLeaderboardStat },
29
- world, physics, camera, worldItem, theme, hud, screens, // screens: start/win/lose
30
- };
31
- export const game = cartridge(config);
32
- ```
33
-
34
- The run is a phase machine every game gets for free: `start → countdown → playing → won | lost`. `flow.start: "gate"` renders `screens.start` (TitleScreen binding) until begin; `flow.countdownSeconds` renders the big-number countdown; `flow.restart: true` registers a `restart` command that resets the run in place, clears cartridge entities, and restores player stats — no hand-rolled session store, phase union, or reset function. `run.playingSeconds` is the run clock (drives the survive rule and the timer panel), so pauses and pre-game phases never skew timing.
35
-
36
- ## Cartridge surface (`@jgengine/shell/cartridge/`)
37
-
38
- The survivor-shaped defaults (XP gems, contact-radius melee, orbit/pulse/projectile weapons) are a genre archetype, not a core primitive — the whole cartridge implementation lives in `@jgengine/shell` (opt-in, alongside its React binding), never in `@jgengine/core`. `core` only supplies the generic pieces it composes: `spawnDirector`, `abilityKit`, `leveling`/`progression`, `runDraft`, `autoTarget`. A puzzle or tactics game imports none of this.
39
-
40
- - `@jgengine/shell/cartridge/spec` — the `CartridgeSpec` types, `Leveled` values (`number | { base, perLevel, min?, max? } | { table }` resolved by `leveled(value, level)`), `WASD_KEYBINDS` (the default when `input` is omitted).
41
- - `@jgengine/shell/cartridge/runtime` — `createCartridge(spec)` → `{ content, loop, run(ctx), weaponKit(ctx), chooseUpgrade(ctx, offerId) }`. Owns the run store (outcome/kills/fields/weapon levels, subscribe/notify), spawn director advance + ring placement, chase + contact-damage enemies (terrain-grounded), the three weapon archetypes with auto-targeting, magnet xp-gem pickups feeding `leveling()`, pause-and-draft level-ups, kill → gem drop + leaderboard, and win/lose rules.
42
- - `@jgengine/shell/cartridge/validate` — `validateCartridge(spec)` → problem list: spawn waves must reference declared enemies, upgrades must reference declared weapons/fields, stacks can't exceed weapon `maxLevel`, tuning must be positive, rarity thresholds descending. The game's test asserts it returns `[]`.
43
-
44
- Weapon archetypes: `projectile` (auto-target nearest in `range`, travel-time bolt fx), `orbit` (leveled blade count/radius sweeping `hitRadius`), `pulse` (radial AoE with linear falloff and an expanding-ring fx). Upgrade effects: `weaponLevel` (+1 level per stack), `statBonus` (+max and +current on pick), `fieldAdd` / `fieldMultiply` (recomputed from stacks). Weapon damage is multiplied by the `damageMultiplier` field when declared.
45
-
46
- ## Presentation (shell + registry)
47
-
48
- `hud.panels` is a schema — items `vital | xp | timer | score | abilityBar | component`, anchored via the movable HUD layout; `screens.win/lose` render the results/death screens; the upgrade-draft modal appears whenever a draft is pending. The shell renders the frame and injects concrete components through the `panels` seam — `standardCartridgePanels` (registry `cartridge-panels`) binds the jgengine UI kit; swap any binding for a custom component without touching the engine. Weapon fx render engine-side from the run store's bolt/pulse feeds (`fxColor`/`fxEmissive` per weapon). `theme` is the `--jg-*` CSS var block — hand-pick it or derive from a few seed colors with `deriveJgTheme` (registry `jg-theme`).
49
-
50
- ## Escape hatches — bespoke logic without leaving the cartridge
51
-
52
- - `weapons.<id> = { kind: "custom", fire(ctx, run, args) }` — cooldown/level/damage handled for you; the fire shape is yours.
53
- - `spawning.placement = { kind: "custom", position(ctx, run) }`.
54
- - `rules.win = { kind: "custom", check(ctx, run) }`.
55
- - `progression.draft.upgrades[].effect = { kind: "custom", apply(ctx, run, stacks) }`.
56
- - `systems: [(ctx, run, dt) => ...]` — extra per-tick simulation after the built-ins, only while playing.
57
- - `hud` items `{ kind: "component", Component }` — arbitrary React panels alongside the schema ones.
58
-
59
- A game that is *mostly* bespoke simulation (custom physics, session machines) should keep `defineGame` and write its systems — the cartridge is for games whose shape the archetypes already cover.
60
-
61
- ## Testing
62
-
63
- Cartridge behaviors are engine-tested once (`packages/shell/src/cartridge/runtime.test.ts`); the game test is one call plus any game-specific assertions:
64
-
65
- ```ts
66
- import { bootCartridge, cartridgeSmokeTest, tickCartridge } from "@jgengine/shell/cartridge/testkit";
67
- cartridgeSmokeTest(config); // validate + world summary + headless run/spawn/kill/gem smoke
68
- ```
69
-
70
- `bootCartridge`/`tickCartridge` build a headless `GameContext` and drive the loop (auto-choosing drafts) for custom assertions — see `@jgengine/shell/cartridge/testkit`. The testkit imports `bun:test`; import it from test files only.
71
-
72
- Barrel surface tokens: createKeyValueStore createUnlocks durabilityFraction isComplete isOverdrawn memorySaveBackend partInSlot pruneToasts policyProjectsViewers projectByVisibleIds projectEntitiesForViewer projectPerUserForViewer visibleEntityIds applyDeltaToSnapshot applySurfaceDeltaToSnapshot boundaryNeighbors catenaryCurve distanceToPolygonEdge footprintObstacles hasValidAdjacency pointInPolygon polygonArea polygonBounds populateNavGridFromEnvironment readScatterPalette readScatterRules revertDeltaFromSnapshot revertSurfaceDeltaFromSnapshot slopeStepCost solveLock solveLockPath