@jgengine/shell 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 (112) hide show
  1. package/CHANGELOG.md +158 -7
  2. package/dist/GamePlayerShell.js +20 -3
  3. package/dist/Shell3dPresentation.d.ts +3 -9
  4. package/dist/Shell3dPresentation.js +26 -6
  5. package/dist/ShellHudPresentation.d.ts +9 -1
  6. package/dist/ShellHudPresentation.js +21 -0
  7. package/dist/audio/AudioComponents.js +6 -2
  8. package/dist/audio/audioEngine.d.ts +8 -0
  9. package/dist/audio/audioEngine.js +65 -6
  10. package/dist/audio/audioWire.d.ts +3 -0
  11. package/dist/audio/audioWire.js +7 -1
  12. package/dist/audio/loopParams.d.ts +39 -0
  13. package/dist/audio/loopParams.js +43 -0
  14. package/dist/camera/GameCameraRig.js +1 -1
  15. package/dist/camera/GameFirstPersonCamera.d.ts +4 -2
  16. package/dist/camera/GameFirstPersonCamera.js +52 -15
  17. package/dist/camera/Viewports.d.ts +34 -0
  18. package/dist/camera/Viewports.js +123 -0
  19. package/dist/camera/cameraRigs.js +70 -62
  20. package/dist/camera/orbitCameraMath.js +2 -2
  21. package/dist/camera/rigMath.d.ts +82 -4
  22. package/dist/camera/rigMath.js +133 -15
  23. package/dist/defineGame.d.ts +9 -0
  24. package/dist/defineGame.js +20 -3
  25. package/dist/devtools/DevtoolsOverlay.js +2 -0
  26. package/dist/devtools/agentBridge.d.ts +6 -0
  27. package/dist/devtools/agentBridge.js +15 -1
  28. package/dist/diagnostics/FpsCounter.d.ts +17 -0
  29. package/dist/diagnostics/FpsCounter.js +32 -0
  30. package/dist/drivers/FrameDriver.js +13 -2
  31. package/dist/drivers/FrameRateLimiter.d.ts +9 -0
  32. package/dist/drivers/FrameRateLimiter.js +39 -0
  33. package/dist/drivers/HudOnlyDriver.js +1 -1
  34. package/dist/environment/DayNightSky.js +5 -5
  35. package/dist/environment/Daylight.d.ts +35 -1
  36. package/dist/environment/Daylight.js +98 -28
  37. package/dist/environment/VolumetricClouds.js +20 -2
  38. package/dist/environment/daylightCycle.d.ts +20 -0
  39. package/dist/environment/daylightCycle.js +59 -7
  40. package/dist/environment/sunShadowMath.d.ts +45 -0
  41. package/dist/environment/sunShadowMath.js +50 -0
  42. package/dist/input/gamepadPoll.d.ts +47 -0
  43. package/dist/input/gamepadPoll.js +142 -0
  44. package/dist/input/gamepadSource.d.ts +11 -1
  45. package/dist/input/gamepadSource.js +117 -71
  46. package/dist/input/mouseLook.js +2 -1
  47. package/dist/input/padHaptics.d.ts +17 -0
  48. package/dist/input/padHaptics.js +32 -0
  49. package/dist/input/pointerLock.d.ts +11 -0
  50. package/dist/input/pointerLock.js +19 -0
  51. package/dist/postfx/PostProcessing.d.ts +1 -1
  52. package/dist/postfx/PostProcessing.js +5 -1
  53. package/dist/postfx/stylizePass.d.ts +23 -0
  54. package/dist/postfx/stylizePass.js +216 -0
  55. package/dist/render/SceneModels.js +1 -1
  56. package/dist/render/SkinnedInstances.d.ts +91 -0
  57. package/dist/render/SkinnedInstances.js +249 -0
  58. package/dist/render/TextureFiltering.d.ts +16 -0
  59. package/dist/render/TextureFiltering.js +64 -0
  60. package/dist/render/displayColor.d.ts +33 -0
  61. package/dist/render/displayColor.js +138 -0
  62. package/dist/render/modelLoad.d.ts +29 -0
  63. package/dist/render/modelLoad.js +61 -1
  64. package/dist/render/resolveModel.js +4 -0
  65. package/dist/render/useFootIk.d.ts +53 -4
  66. package/dist/render/useFootIk.js +197 -70
  67. package/dist/render/useModelAnimation.d.ts +24 -0
  68. package/dist/render/useModelAnimation.js +61 -40
  69. package/dist/scene/AuthoredScene.d.ts +9 -0
  70. package/dist/scene/AuthoredScene.js +17 -1
  71. package/dist/scene/CityRenderer.js +6 -6
  72. package/dist/settings/QuickControls.js +1 -1
  73. package/dist/settings/SettingsMenu.js +1 -1
  74. package/dist/settings/appliedSettings.d.ts +9 -3
  75. package/dist/settings/appliedSettings.js +6 -5
  76. package/dist/settings/settingsController.d.ts +3 -0
  77. package/dist/settings/settingsController.js +47 -3
  78. package/dist/structures/BuildingKitBatch.js +26 -22
  79. package/dist/structures/GeneratedBuilding.d.ts +3 -15
  80. package/dist/structures/GeneratedBuilding.js +21 -15
  81. package/dist/structures/buildingSurface.d.ts +21 -0
  82. package/dist/structures/buildingSurface.js +134 -0
  83. package/dist/useShellMultiplayerSync.d.ts +1 -1
  84. package/dist/useShellMultiplayerSync.js +5 -3
  85. package/dist/world/WorldParticles.js +1 -1
  86. package/package.json +4 -4
  87. package/skills/ce-handoff/SKILL.md +1 -1
  88. package/skills/game-design/SKILL.md +2 -2
  89. package/skills/jgengine/SKILL.md +7 -5
  90. package/skills/jgengine/api.md +118 -89
  91. package/skills/jgengine/capabilities.md +28 -0
  92. package/skills/jgengine/recipes/minimal-game.md +3 -0
  93. package/skills/jgengine-ui/SKILL.md +13 -16
  94. package/skills/jgengine-ui/api.md +418 -317
  95. package/skills/jgengine-ui/capabilities.md +16 -0
  96. package/skills/jgengine-ui/reference-primitives.md +89 -0
  97. package/skills/jgengine-ui/reference.md +2 -81
  98. package/skills/jgengine-world/SKILL.md +15 -3
  99. package/skills/jgengine-world/api.md +873 -626
  100. package/skills/jgengine-world/capabilities.md +130 -6
  101. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  102. package/skills/jgengine-world/recipes/controls.md +44 -0
  103. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  104. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  105. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  106. package/skills/jgengine-world/reference.md +31 -2
  107. package/skills/jgengine-world/visibility.md +112 -0
  108. package/skills/level-design/SKILL.md +1 -1
  109. package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
  110. package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
  111. package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
  112. package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
@@ -4,7 +4,11 @@
4
4
 
5
5
  Reach for these before hand-rolling. Each row is *the thing you need* → *the primitive that already does it*.
6
6
 
7
- ## ai-decision-graph — Evaluate serializable selector, sequence, condition, action, and utility AI decisions.
7
+ ## action-contexts — Swap bindings live for on-foot, driving, menu or build modes by pushing and popping layered action maps.
8
+
9
+ - `createActionContextStack` (function) · `import { createActionContextStack } from "@jgengine/core/input/actionContexts"`
10
+
11
+ ## ai-decision-graph — Evaluate serializable selector, sequence, condition, action, utility, timer, and random AI decisions with interrupt hooks.
8
12
 
9
13
  - `createDecisionGraphRuntime` (function) · `import { createDecisionGraphRuntime } from "@jgengine/core/ai/decisionGraph"`
10
14
 
@@ -20,13 +24,22 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
20
24
 
21
25
  - `createDriverState` (function) · `import { createDriverState } from "@jgengine/core/ai/driver"`
22
26
 
27
+ ## air-metrics — measure a vehicle's jumps and air control — apex height and time, double jump, air rotation rates
28
+
29
+ - `measureAir` (function) · `import { measureAir } from "@jgengine/core/physics/handlingProbe"`
30
+
23
31
  ## anim-graph — data-first animation state machine with blend trees, crossfades, layers, and clip events
24
32
 
25
33
  - `createAnimGraphRuntime` (function) · `import { createAnimGraphRuntime } from "@jgengine/core/anim/animGraph"`
34
+ - `parseAnimGraph` (function) · `import { parseAnimGraph } from "@jgengine/core/anim/animGraph"`
26
35
 
27
- ## animation — inverse kinematics orient a bone toward a target with constrained yaw and pitch
36
+ ## animation — find thigh, shin and foot bones on a humanoid rig for foot IK
28
37
 
38
+ - `inferLegChains` (function) · `import { inferLegChains } from "@jgengine/core/anim/footPlacement"`
29
39
  - `lookAt` (function) · `import { lookAt } from "@jgengine/core/anim/ikSolver"`
40
+ - `placeFeet` (function) · `import { placeFeet } from "@jgengine/core/anim/footPlacement"`
41
+ - `planBoneTexture` (function) · `import { planBoneTexture } from "@jgengine/core/anim/boneTexture"`
42
+ - `sampleBoneTexture` (function) · `import { sampleBoneTexture } from "@jgengine/core/anim/boneTexture"`
30
43
  - `solveFabrik` (function) · `import { solveFabrik } from "@jgengine/core/anim/ikSolver"`
31
44
  - `solveTwoBone` (function) · `import { solveTwoBone } from "@jgengine/core/anim/ikSolver"`
32
45
 
@@ -101,6 +114,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
101
114
  - `placeAuthoredObjects` (function) · `import { placeAuthoredObjects } from "@jgengine/core/world"`
102
115
  - `resolveAuthoredObjects` (function) · `import { resolveAuthoredObjects } from "@jgengine/core/world"`
103
116
 
117
+ ## authored-solids — collision for studio-authored world content such as city volumes
118
+
119
+ - `resolveAuthoredSolids` (function) · `import { resolveAuthoredSolids } from "@jgengine/core/world/authoredSolids"`
120
+ - `syncAuthoredSolids` (function) · `import { syncAuthoredSolids } from "@jgengine/core/world/authoredSolids"`
121
+
104
122
  ## authored-spawn — read spawn points and markers from the editor document
105
123
 
106
124
  - `authoredSpawnPosition` (function) · `import { authoredSpawnPosition } from "@jgengine/core/world/authoredSpawn"`
@@ -116,6 +134,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
116
134
  - `registerBuiltinTriggerActions` (function) · `import { registerBuiltinTriggerActions } from "@jgengine/core/scene/authoredTriggers"`
117
135
  - `registerTriggerAction` (function) · `import { registerTriggerAction } from "@jgengine/core/world"`
118
136
 
137
+ ## axis-shaping — shape raw input per axis — deadzone, response curve, keyboard ramp, self-centring, speed-sensitive scaling
138
+
139
+ - `createAxisShaper` (function) · `import { createAxisShaper } from "@jgengine/core/input/axisShaper"`
140
+
119
141
  ## behavior-actions — register named callbacks for decision graph behavior descriptors
120
142
 
121
143
  - `registerBehaviorActions` (function) · `import { registerBehaviorActions } from "@jgengine/core/world"`
@@ -128,6 +150,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
128
150
 
129
151
  - `advanceBehaviors` (function) · `import { advanceBehaviors } from "@jgengine/core/world"`
130
152
 
153
+ ## boat-dynamics — force-based powered hull — hull-speed drag hump, planing, keel, rudder that needs flow or outboard thrust
154
+
155
+ - `createBoatDynamics` (function) · `import { createBoatDynamics } from "@jgengine/core/physics/boatDynamics"`
156
+
131
157
  ## body-bind — mirror sim-body snapshots onto scene entities each tick, no per-body setPose
132
158
 
133
159
  - `createBodyBind` (function) · `import { createBodyBind } from "@jgengine/core/world"`
@@ -167,6 +193,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
167
193
  ## city-district — polygon math for road-derived blocks, parcels, and buildable footprints
168
194
 
169
195
  - `Vec2` (type) · `import { Vec2 } from "@jgengine/core/world/cityGeometry"`
196
+ - `citySolids` (function) · `import { citySolids } from "@jgengine/core/world/cityKind"`
170
197
  - `extractGraphBlocks` (function) · `import { extractGraphBlocks } from "@jgengine/core/world/cityBlocks"`
171
198
  - `rectClearsPolyline` (function) · `import { rectClearsPolyline } from "@jgengine/core/world/cityGeometry"`
172
199
  - `rectsSeparated` (function) · `import { rectsSeparated } from "@jgengine/core/world/cityGeometry"`
@@ -193,6 +220,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
193
220
 
194
221
  - `createTimerSet` (function) · `import { createTimerSet } from "@jgengine/core/world"`
195
222
 
223
+ ## course-metrics — measure a vehicle on driven tests — understeer gradient, skidpad g, slalom speed
224
+
225
+ - `measureCourse` (function) · `import { measureCourse } from "@jgengine/core/physics/handlingProbe"`
226
+
196
227
  ## crouch-prone — stance/pose transitions that change the hitbox
197
228
 
198
229
  - `createPoseState` (function) · `import { createPoseState } from "@jgengine/core/world"`
@@ -241,6 +272,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
241
272
 
242
273
  - `DIFFICULTY_TIERS` (const) · `import { DIFFICULTY_TIERS } from "@jgengine/core/ai/difficulty"`
243
274
 
275
+ ## doppler-pitch — pitch a moving sound up as it approaches and down as it passes, from listener and emitter velocity
276
+
277
+ - `dopplerRate` (function) · `import { dopplerRate } from "@jgengine/core/audio/doppler"`
278
+
279
+ ## drivable-vehicle — drive a car from throttle/steer/handbrake input each tick: step the vehicle sim and pose its entity
280
+
281
+ - `tickDrivableVehicle` (function) · `import { tickDrivableVehicle } from "@jgengine/core/world"`
282
+
244
283
  ## elevation-readout — cursor height and delta-from-reference feedback
245
284
 
246
285
  - `sampleElevation` (function) · `import { sampleElevation } from "@jgengine/core/world"`
@@ -249,6 +288,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
249
288
 
250
289
  - `summarizeElevation` (function) · `import { summarizeElevation } from "@jgengine/core/world"`
251
290
 
291
+ ## engine-layers — layered engine audio: rpm-keyed samples crossfaded, with on-load and off-load sets picked by engine load
292
+
293
+ - `createEngineLayers` (function) · `import { createEngineLayers } from "@jgengine/core/audio/engineLayers"`
294
+
252
295
  ## entity-meta — cast-free narrow of SceneEntity.meta via a type guard
253
296
 
254
297
  - `entityMetaOf` (function) · `import { entityMetaOf } from "@jgengine/core/world"`
@@ -257,9 +300,17 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
257
300
 
258
301
  - `createFastTravelNetwork` (function) · `import { createFastTravelNetwork } from "@jgengine/core/world"`
259
302
 
260
- ## flight-dynamics — simulate fixed-wing, helicopter, and VTOL aircraft
303
+ ## feedback-mixer — map sim telemetry to camera, audio and haptic parameters through curves, smoothing and threshold events
304
+
305
+ - `createFeedbackMixer` (function) · `import { createFeedbackMixer } from "@jgengine/core/vfx/feedbackMixer"`
306
+
307
+ ## fire-input — bind a key or mouse button to a named action that runs the same-named command with the aim; repeatMs auto-fires while held
308
+
309
+ - `ActionCodesMap` (type) · `import { ActionCodesMap } from "@jgengine/core/input/actionBindings"`
261
310
 
262
- - `createAircraftDynamics` (function) · `import { createAircraftDynamics } from "@jgengine/core/world"`
311
+ ## flight-metrics — measure an aircraft's feel as numbers — roll rate, sustained turn, stall speed, climb, throttle response, hover drift
312
+
313
+ - `measureFlight` (function) · `import { measureFlight } from "@jgengine/core/physics/handlingProbe"`
263
314
 
264
315
  ## follow-trail — trailing follower/snake formation that chases a leader
265
316
 
@@ -327,10 +378,22 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
327
378
 
328
379
  - `grounded` (function) · `import { grounded } from "@jgengine/core/world"`
329
380
 
381
+ ## handling-metrics — measure a vehicle's feel as numbers — 0–100, braking, lateral g, turn-in, sideslip, spin
382
+
383
+ - `measureHandling` (function) · `import { measureHandling } from "@jgengine/core/physics/handlingProbe"`
384
+
385
+ ## haptics — Continuous gamepad rumble from named channels (engine, road, impact) mixed by priority, plus fading pulses.
386
+
387
+ - `createHapticChannels` (function) · `import { createHapticChannels } from "@jgengine/core/input/haptics"`
388
+
330
389
  ## hold-order — stand-ground order that holds position until preempted or canceled
331
390
 
332
391
  - `defineHoldOrder` (function) · `import { defineHoldOrder } from "@jgengine/core/world"`
333
392
 
393
+ ## input-buffer — buffer early presses, coyote time, hold duration and double tap for responsive controls
394
+
395
+ - `createInputBuffer` (function) · `import { createInputBuffer } from "@jgengine/core/input/inputBuffer"`
396
+
334
397
  ## interaction-prompt — observable nearest-interactable prompt registry over the proximity-prompt resolver — register/resolve/active with priority+range and change-notified snapshot/restore
335
398
 
336
399
  - `createPromptRegistry` (function) · `import { createPromptRegistry } from "@jgengine/core/world"`
@@ -355,6 +418,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
355
418
 
356
419
  - `createRebindSession` (function) · `import { createRebindSession } from "@jgengine/core/input/rebindSession"`
357
420
 
421
+ ## kinematic-vehicle — arcade car/kart driving physics with throttle, brake, steer and retunable handling
422
+
423
+ - `createKinematicVehicle` (function) · `import { createKinematicVehicle } from "@jgengine/core/world"`
424
+
425
+ ## lean-metrics — measure a motorcycle's lean — steady lean, time to lean, countersteer tip-in
426
+
427
+ - `measureLean` (function) · `import { measureLean } from "@jgengine/core/physics/handlingProbe"`
428
+
358
429
  ## limb-health — per-body-part/region health tracked separately
359
430
 
360
431
  - `createMultiRegionHealth` (function) · `import { createMultiRegionHealth } from "@jgengine/core/procedural"`
@@ -367,8 +438,9 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
367
438
 
368
439
  - `generateLock` (function) · `import { generateLock } from "@jgengine/core/world"`
369
440
 
370
- ## locomotion-graph — build the default idle/walk/run plus one-shot graph from clip names
441
+ ## locomotion-graph — resolve the animation graph a model config plays
371
442
 
443
+ - `animGraphFromConfig` (function) · `import { animGraphFromConfig } from "@jgengine/core/anim/locomotionGraph"`
372
444
  - `locomotionGraph` (function) · `import { locomotionGraph } from "@jgengine/core/anim/locomotionGraph"`
373
445
 
374
446
  ## map-annotations — player-drawn map annotation layer — freehand strokes, area shapes, and pinned notes, projected into the map's routes/zones props and serializable
@@ -400,6 +472,22 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
400
472
 
401
473
  - `defineMoveOrder` (function) · `import { defineMoveOrder } from "@jgengine/core/world"`
402
474
 
475
+ ## movement-metrics — measure a walking character's feel as numbers — time to top speed, stop distance, jump height and apex, air control, turn-around
476
+
477
+ - `measureMovement` (function) · `import { measureMovement } from "@jgengine/core/movement/movementProbe"`
478
+
479
+ ## nav-from-solids — block world solids on a nav grid
480
+
481
+ - `populateNavGridFromSolids` (function) · `import { populateNavGridFromSolids } from "@jgengine/core/nav/navFromEnvironment"`
482
+
483
+ ## navmesh-bake — voxelize scene triangles into an agent-sized polygon nav mesh
484
+
485
+ - `bakeNavMesh` (function) · `import { bakeNavMesh } from "@jgengine/navbake"`
486
+
487
+ ## navmesh-query — route, snap, and raycast on a polygon navmesh with retunable area costs and a bounded search
488
+
489
+ - `createNavMeshQuery` (function) · `import { createNavMeshQuery } from "@jgengine/core/world"`
490
+
403
491
  ## object-slot-inventory — placed objects hold validated per-instance container contents that replicate and save with the placement
404
492
 
405
493
  - `SceneObjectSlots` (interface) · `import { SceneObjectSlots } from "@jgengine/core/world"`
@@ -497,7 +585,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
497
585
 
498
586
  - `resolveActivePrompt` (function) · `import { resolveActivePrompt } from "@jgengine/core/world"`
499
587
 
500
- ## pursuit — chase-to-reach cooldown-gated attack with optional leash-to-home; owns the attack cooldown
588
+ ## pursuit — enemy/mob aggro chase-to-reach with a cooldown-gated attack and optional leash-to-home; owns the attack cooldown
501
589
 
502
590
  - `advancePursuit` (function) · `import { advancePursuit } from "@jgengine/core/ai/pursuit"`
503
591
 
@@ -521,6 +609,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
521
609
 
522
610
  - `createResourceNodeField` (function) · `import { createResourceNodeField } from "@jgengine/core/world/resourceNode"`
523
611
 
612
+ ## ride-metrics — measure a sprung vehicle's ride — settle time, dive, roll per g, landing bounce
613
+
614
+ - `measureRide` (function) · `import { measureRide } from "@jgengine/core/physics/handlingProbe"`
615
+
616
+ ## rigid-aircraft — force-and-torque aircraft — lifting surfaces with stall and drag polar, control surfaces, inertia, loops and rolls
617
+
618
+ - `createRigidAircraft` (function) · `import { createRigidAircraft } from "@jgengine/core/physics/aircraftDynamics"`
619
+
524
620
  ## rts-control-groups — map Ctrl+digit / digit / double-tap presses onto selection-bookmark bind / recall / focus intents
525
621
 
526
622
  - `controlGroupKey` (function) · `import { controlGroupKey } from "@jgengine/core/world"`
@@ -560,6 +656,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
560
656
 
561
657
  ## solid-obstacles — the one blocking-geometry query the player resolver and NPC movers share
562
658
 
659
+ - `obstacleFromSolid` (function) · `import { obstacleFromSolid } from "@jgengine/core/movement/solidObstacles"`
563
660
  - `solidObstaclesNear` (function) · `import { solidObstaclesNear } from "@jgengine/core/movement/solidObstacles"`
564
661
 
565
662
  ## spatial-region-box — sample uniformly within an axis-aligned 3D box
@@ -683,6 +780,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
683
780
  - `setTouchControlsMode` (function) · `import { setTouchControlsMode } from "@jgengine/core/input/touchControlsMode"`
684
781
  - `touchButtonShape` (function) · `import { touchButtonShape } from "@jgengine/core/input/touchScheme"`
685
782
 
783
+ ## vehicle-backend-link — collide a vehicle sim with a physics backend — shove props, stop at walls, slide along them
784
+
785
+ - `createVehicleBackendLink` (function) · `import { createVehicleBackendLink } from "@jgengine/core/physics/vehicleBackendLink"`
786
+
787
+ ## vehicle-dynamics — force-based car handling — tires, load transfer, drivetrain, assists — tuned with physical numbers
788
+
789
+ - `createVehicleDynamics` (function) · `import { createVehicleDynamics } from "@jgengine/core/physics/vehicleDynamics"`
790
+
791
+ ## vehicle-seats — enter, exit and swap seats in a car or mount (GTA-style boarding) with driver and passenger slots
792
+
793
+ - `createVehicleSeats` (function) · `import { createVehicleSeats } from "@jgengine/core/world"`
794
+
686
795
  ## visibility-field — per-viewer-group observed/remembered/hidden fog-of-war knowledge with terrain memory and authoritative entity disclosure
687
796
 
688
797
  - `createVisibilityField` (function) · `import { createVisibilityField } from "@jgengine/core/world"`
@@ -729,3 +838,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
729
838
  ## world-seed — derive a deterministic generator/scatter seed from world id + save/run seed
730
839
 
731
840
  - `seedForPlace` (function) · `import { seedForPlace } from "@jgengine/core/world"`
841
+
842
+ ## world-solid-bounds — world AABB and nav footprint of a world solid
843
+
844
+ - `worldSolidBounds` (function) · `import { worldSolidBounds } from "@jgengine/core/world/worldSolids"`
845
+ - `worldSolidFootprint` (function) · `import { worldSolidFootprint } from "@jgengine/core/world/worldSolids"`
846
+
847
+ ## world-solids — collision for generated and authored world geometry (buildings, city lots, walls)
848
+
849
+ - `createWorldSolids` (function) · `import { createWorldSolids } from "@jgengine/core/world/worldSolids"`
850
+
851
+ ## world-solids-from-data — derive collision boxes from generated buildings and wall runs
852
+
853
+ - `buildingSolids` (function) · `import { buildingSolids } from "@jgengine/core/world/worldSolids"`
854
+ - `structureSolids` (function) · `import { structureSolids } from "@jgengine/core/world/worldSolids"`
855
+ - `wallSolids` (function) · `import { wallSolids } from "@jgengine/core/world/worldSolids"`
@@ -0,0 +1,39 @@
1
+ # Recipe — character animation (graph → rig → ground)
2
+
3
+ **What this wires:** a rigged character that moves the way the game plays: clips chosen by data, feet that meet the ground, and gameplay moments keyed to animation frames. There is no default hero, soldier or NPC rig: every game picks its clips, parameters and corrections.
4
+
5
+ ## The seams
6
+
7
+ - **Clips on a model.** `ModelConfig.animation` on an entity kind. A catalog model with indexed `clips` animates automatically through semantic clip roles; `"none"` holds the bind pose. `{ clip, paused, time }` holds one pose.
8
+ - **Graph.** `animation.graph` is an `AnimGraph` (`@jgengine/core/anim/animGraph`): layers, each a state machine of `clip`, `blend1D` and `blend2D` states joined by transitions with `when` conditions, `trigger`s, crossfade `duration` and `exitTime`. It is plain data, so it saves, diffs and can be authored in the editor.
9
+ - `locomotionGraph({ idle, walk, run, walkSpeed, runSpeed, fadeSec, oneShots })` builds the idle/walk/run blend plus one-shots; `states`/`oneShots` configs are converted through it.
10
+ - Upper-body actions (aim, reload, wave) go on a second layer with `mask: ["spine", "arm", ...]` bone prefixes, `additive` for recoil or breathing on top of the base.
11
+ - `createAnimGraphRuntime(graph)` runs the same graph headless: `advance(dt, params, clips)`, `trigger`, `snapshot`/`restore`, `retune`. Test transitions with it before looking at pixels.
12
+ - **Authoring in the editor.** A placement's graph is inspected, previewed and stored from the editor's Animation workspace (see `jgengine-editor` reference, "Placement animation"). `animGraphFromConfig(config)` gives the graph any config plays, so tools and tests see what the shell plays. `parseAnimGraph(json)` validates a stored one.
13
+ - **Parameters.** The shell feeds the entity's smoothed ground speed as `speed`. Anything else (aiming, crouched, strafe x/y) goes on the entity blackboard under `ANIM_PARAMS_KEY`.
14
+ - **Triggers.** `ctx.game.playEntityAnimation(id, name)` arms a trigger; `hit` and `death` arm from their combat events.
15
+ - **Events.** `graph.events: [{ clip, atSec, name }]` emit `animation.event` when a clip crosses that time. Hook footstep audio, the damage frame of a swing or the magazine swap of a reload to the event, not to a timer.
16
+ - **Root motion.** `rootMotion: true` on a state moves the entity by the clip's horizontal root-bone travel, turned by the entity's facing and scaled with the model, instead of playing it in place. The root bone's vertical motion stays in the clip, and other states keep their root animation untouched. Use it for dodges, vaults and attacks with lunges whose distance must match the feet. `output.rootMotion` reports when a root-motion state is current.
17
+ - **Foot IK.** `ModelConfig.ik` runs after the mixer each frame.
18
+ - `ik: "auto"` finds thigh → shin → foot chains by bone name. Pass `{ feet: [{ root, mid, tip }] }` when the rig's names are unusual, and `feet: []` to keep only `lookAt`.
19
+ - Each foot keeps its animated swing and moves by its ground's height above or below the model origin. The pelvis drops so the lower leg can reach, soles never end under the ground, and planted feet tilt to the ground normal (`alignToGround`, `0..1`). The correction fades out while the feet are clear of the ground.
20
+ - `maxAdjust` caps the correction as a fraction of leg length (default `0.4`). Probes hit terrain and blocking physical objects, never the model's own entity.
21
+ - The pure pieces are `placeFeet` and `inferLegChains` (`@jgengine/core/anim/footPlacement`) and `solveTwoBone`/`solveFabrik`/`lookAt` (`@jgengine/core/anim/ikSolver`), for a custom host or a non-leg chain (a hand on a ledge, a tail).
22
+ - **Crowds.** `<SkinnedInstances url instances clips once fps modelScale />` (`@jgengine/shell/render/SkinnedInstances`) draws hundreds of animated copies of one rig in one instanced draw per material. Each `CrowdInstance` has `position`, `rotationY`, `scale`, `clip`, `timeOffset` and `speed`.
23
+ - Every clip is baked once to a bone-matrix texture (layout from `planBoneTexture`, `@jgengine/core/anim/boneTexture`) and sampled on the GPU. Clips in `once` hold their last pose. A member changes clip when the game passes a new `instances` array.
24
+ - Only skinned meshes are baked. Static props parented to bones (a weapon in a hand slot, a hat) are dropped, and a mesh with several materials keeps its first.
25
+ - Crowd members have no graph, IK or attachments. Promote the few near the camera or near the player to entity models.
26
+ - `bakeSkinnedCrowd` and `patchCrowdMaterial` are exported for a custom renderer.
27
+
28
+ ## Symptom → knob
29
+
30
+ | Symptom | Knob |
31
+ | --- | --- |
32
+ | Feet slide at walk or run speed | `walkSpeed`/`runSpeed` to the clip's authored stride speed, or blend1D `at` points |
33
+ | Pop between states | transition `duration`; `exitTime` on one-shots returning to locomotion |
34
+ | Feet float or sink on slopes and steps | `ik: "auto"` |
35
+ | Knees buckle on steep ground | lower `ik.maxAdjust` |
36
+ | Feet tilt wrongly on rubble or props | lower `ik.alignToGround` |
37
+ | Attack damage lands before the swing | `graph.events` on the hit frame instead of a timer |
38
+ | Lunge distance doesn't match the feet | `rootMotion: true` on that state |
39
+ | Hundreds of animated characters cost thousands of draw calls | `SkinnedInstances` for the background crowd |
@@ -0,0 +1,44 @@
1
+ # Recipe — controls (bindings → contexts → devices → feel)
2
+
3
+ **What this wires:** a game's controls from small parts: named actions, layered contexts that swap bindings live, keyboard/mouse and gamepad on the same actions, and per-axis feel. There are no control presets; every game names its own actions and codes.
4
+
5
+ ## The seams
6
+
7
+ - **Actions.** `defineGame({ input: { action: codes } })` maps action names to codes. Keyboard codes are `KeyboardEvent.code` (`KeyW`), pad buttons are `pad:<index>` and stick or trigger directions are `padaxis:<index>+`/`-` in the standard mapping. `bindingLabel(code, "xbox")` gives the glyph text for prompts. Sim code reads actions only: `ctx.input.isDown`, `justPressed`, `value` (0..1, analog when a stick or trigger drives it) and `axis(bindings)`.
8
+ - **Contexts.** `actionContextStack(ctx)` (`@jgengine/core/game/controlGate`) layers action maps over the base input. `push({ id, codes, passthrough, axes?, shaping? })` swaps bindings immediately: the shell re-binds keyboard, touch and pad on the next frame, and a key held across the swap keeps counting toward whatever it maps to now. `passthrough: false` hides everything below (menus, a parked vehicle); `passthrough: true` adds or overrides a few actions (a build mode on top of on-foot). The stack has `snapshot`/`restore`, so the current mode survives a save.
9
+ - **Axes and feel per context.** A context can carry `axes` (axis bindings by action name) and `shaping` (serializable `createAxisShaper` profiles). Read them with `activeAxes()` when `version()` changes and `retune` the shaper, so on-foot movement and driving steer each get their own ramp and curve without a branch per mode.
10
+ - **Pad feel.** `defineGame({ gamepad: { deadzone, curve, triggerDeadzone } })` shapes sticks and triggers before values reach `ctx.input`; the axis shaper only adds ramps and speed scaling on top.
11
+ - **Timing.** `createInputBuffer` (`@jgengine/core/input/inputBuffer`) buffers early presses, coyote time, hold duration and double taps.
12
+ - **Local seats (couch co-op).** `defineGame({ localPlayers: { maxSlots: 4 } })` opens seats on one screen. The first pad to press a button shares the primary seat with keyboard and touch (`claimPrimary: "none"` keeps the primary keyboard-only); each further pad hot-joins its own seat, and the shell calls `loop.onNewPlayer(ctx, { userId, isNew: true })` for it, so spawn with `player?.userId ?? ctx.player.userId`. Read a seat with `localPlayers(ctx).local(slotId)` → `{ userId, input }` (`@jgengine/core/runtime/localPlayers`); iterate `slots()` in `onTick`. Seats use the same bindings and contexts as the primary, and the shell's walk controller moves every seat's entity. The table has `snapshot`/`restore`/`retune`.
13
+ - **Split-screen.** `defineGame({ viewports: { layout: "auto", split: "vertical" } })` draws one viewport per joined seat (`@jgengine/core/game/viewports`: `splitViewports`, `resolveViewports`, `viewportPixels`). The primary seat keeps the main camera rig; other seats get a chase or top-down follow camera (`viewports.camera`, defaulting to top-down when the main rig is), and the walk controller moves each seat relative to its own camera. `ViewportHuds` (`@jgengine/shell/camera/Viewports`) gives each seat its own HUD root. Post-processing is skipped while the screen is split. `bun run drive couch --param gamepad=2` shows two seats.
14
+ - **Haptics.** `ctx.input.haptics(userId)` holds named rumble channels: `set(name, { strong, weak }, priority?)` each tick for continuous feel, `pulse(name, { strong, weak, ms }, priority?)` for a fading hit. Higher priority ducks lower channels. The shell drives each seat's own pad; channels are local and never saved or sent.
15
+ - **Rebinding.** `createRebindSession` drives a conflict-aware "press a key" flow; overrides persist per game and apply under the context stack.
16
+
17
+ ## Wiring a mode swap
18
+
19
+ ```ts
20
+ const DRIVING: ActionContext = {
21
+ id: "driving",
22
+ codes: { exit: ["KeyF", "pad:3"] },
23
+ passthrough: true,
24
+ axes: { steer: { positive: ["steerRight"], negative: ["steerLeft"] } },
25
+ shaping: { steer: { digital: { riseRate: 3, returnRate: 6 }, analog: { deadzone: 0.08, curve: 1.5 } } },
26
+ };
27
+
28
+ // enter: actionContextStack(ctx).push(DRIVING); leave: actionContextStack(ctx).pop("driving")
29
+ if (state.contextVersion !== contexts.version()) {
30
+ state.contextVersion = contexts.version();
31
+ const layer = contexts.activeAxes();
32
+ state.bindings = { steer: layer.bindings.steer ?? { positive: [] } };
33
+ state.shaper.retune({ axes: { steer: layer.shaping.steer ?? {} } });
34
+ }
35
+ const raw = ctx.input.axis(state.bindings);
36
+ const axis = state.shaper.shape(dt, raw, { analog: analogAxes(state.bindings, ctx.input.analog()) });
37
+ ```
38
+
39
+ `bun run drive handling` runs this: F pops the driving context and a held W stops reaching the engine.
40
+
41
+ ## Pitfalls
42
+
43
+ - Actions that exist only inside a context are published to `ctx.input` while the context is active, but the base `input` map is what the touch dock and settings menu list. Keep an action in the base map when touch players need it.
44
+ - `shaping` has no `scale` callback because contexts are serializable; apply speed-sensitive scaling on the shaper the game owns.
@@ -0,0 +1,76 @@
1
+ # Recipe — flight feel (aircraft as physical numbers)
2
+
3
+ **What this wires:** an aircraft that flies the way the brief says. There is no default jet or plane: every aircraft is a mass, an inertia tensor, a set of surfaces and an engine the game picks, checked against numbers it writes down first.
4
+
5
+ ## The seams
6
+
7
+ - **Sim.** `createRigidAircraft(tuning, { position, velocity, heading | orientation, wind?, groundHeight?, gravityField?, airDensity? })` from `@jgengine/core/physics/aircraftDynamics`. It is a quaternion rigid body: surface forces act at their mount points on the inertia tensor, so nothing commands a rotation rate. `snapshot()`/`restore()` for prediction and replay, `retune()` for damage, upgrades and staging, per-tick `modifiers` (`thrustScale`, `liftScale`, `dragScale`, extra `force`/`torque`).
8
+ - **Body frame.** `[left, up, forward]` about the centre of mass. A wing panel sits at `[±semiSpan/2, 0, 0]`, a tailplane at `[0, 0, -tailArm]`, a fin at `[0, finHeight, -tailArm]` with `normal: [1, 0, 0]`.
9
+ - **Surfaces.** Each one has `area` (m²), `liftSlope` (per rad), `stallAngle` (rad), `postStallLift` (share of peak kept past the stall), `incidence` (trim), and the drag polar `cd0` + `inducedDrag·CL²`. Split the wing into left and right panels so ailerons have a roll arm.
10
+ - **Controls.** `control: { pitch?, roll?, yaw? }` on a surface is AoA change per rad of that channel's deflection. The sim signs it from the lever arm, so positive input is always nose up, right roll, nose right. Elevons are one surface with both `pitch` and `roll`. `controls.<channel>` sets `maxDeflection` and actuator `rate`.
11
+ - **Rotors.** A `rotor` block makes the same body a helicopter: `maxThrust` (N, full collective and rotor speed), `radius` (m), hub `at`, `torque` (N·m at full collective, positive yaws the nose left), `cyclic` disc tilt, `damping`, `spoolRate` and an optional `tail` rotor (`maxThrust`, `at`, `sideDamping`).
12
+ - Throttle spins the rotor up; `input.collective` sets thrust; the pitch and roll channels tilt the disc; yaw drives the tail rotor.
13
+ - Neutral pedal gives no tail thrust, so the nose swings with the torque until the pilot (or SAS) holds it. Hover pedal is `torque·collective / (tail.maxThrust · tailArm)`.
14
+ - Thrust follows momentum theory: translational lift once clean air reaches the induced velocity `√(T/2ρA)`, less thrust while climbing through the disc, and Cheeseman–Bennett ground effect below about one rotor diameter.
15
+ - The step's `rotor` telemetry (`speed`, `thrust`, `torque`, `groundEffect`, `translationalLift`) drives rotor sound, dust and HUD.
16
+ - **Rockets.** A `motor` block: `thrustCurve` (`[seconds, N]` points over burn time), `propellantKg` on top of `massKg`, `massFlow` (kg/s at peak thrust; it scales with thrust), nozzle `at` and `gimbal` (rad, on the pitch and yaw actuators).
17
+ - Throttle above zero burns and advances the burn clock; throttle `0` coasts with the clock held.
18
+ - Mass and inertia fall as propellant leaves (inertia scales with total over dry mass), so acceleration and turn rate rise through the burn.
19
+ - Stage by `retune(nextStage)`: a new `motor` object loads its propellant and restarts its clock; the same object keeps both. Move the pose to the new stage's centre of mass with `restore` first if the stages differ in length.
20
+ - The step reports `massKg` and `motor` (`thrust`, `burnTime`, `propellantKg`, `burnedOut`).
21
+ - **Assists.** An `assists` block is a flight computer on the same actuators, so it never exceeds the airframe's authority, and full stick on an axis is always the pilot's.
22
+ - `sas: { pitch, roll, yaw }` (`0..1`) damps body rates and, hands-off, holds the attitude or heading at release with a learned trim. `sasRate` sets how firm (rad/s for full control). On a helicopter, `sas.yaw` holds heading against rotor torque with no pedal.
23
+ - `autoLevel` (`0..1`) flies the wings level and the nose to the horizon hands-off.
24
+ - `maxAngleOfAttack` (rad) and `maxG` cut the pitch command before the limit; `step.limited` reports it.
25
+ - `hoverHold` (`0..1`) flies hands-off cyclic to a stop over the ground: position → velocity → attitude → rate.
26
+ - `policy(context, command)` runs last and returns the command the actuators get: replace any built-in with the game's own.
27
+ - `step.command` is what the actuators got. Toggle assists with `retune({ ...aircraft.tuning(), assists })`.
28
+ - **Pose.** Set the entity's `rotationY` from `step.heading` and apply the rest of `step.orientation` to the mesh (the heading-free part: `qY(-heading) · orientation`). `step.pitch`/`step.bank` match three.js `Euler(-pitch, heading, bank, "YXZ")`; `aircraftAttitudeQuaternion(heading, pitch, bank)` goes the other way for spawns.
29
+ - **Camera.** The chase rig (`rig: "chase"`) follows heading; give it `frustum: { far }` in the thousands and a `yawResponse` around 4 so the boom swings smoothly over the top of a loop.
30
+ - **Telemetry.** `airspeed`, `angleOfAttack`, `sideslip`, `gLoad`, `stalled`/`stallFraction`, `thrust`, body rates and `deflection` drive the HUD, stall horn, wind noise and camera shake.
31
+
32
+ ## Workflow
33
+
34
+ 1. **Write the feel as numbers** on the `FlightReport` fields. Some examples:
35
+ - "Nimble aerobatic" is `rollRateDeg > 300` and `sustainedTurnRateDeg > 18`.
36
+ - "Heavy transport" is `rollRateDeg` 40–80 and `stallSpeed` 50–70.
37
+ - "Responsive jet" is `throttleResponse < 2`.
38
+ - "Easy hover" is `hoverDriftMeters < 5` with the game's assists on.
39
+ 2. **Put them in a test** next to the tuning: `measureFlight((spawn) => createRigidAircraft(tuning, spawn), { cruiseSpeed })` from `@jgengine/core/physics/handlingProbe`. It flies deterministic autopilots through a full-aileron roll, full-throttle banked turns, an idle deceleration, a speed-held climb, a throttle step and a hands-off hover. Fields that don't apply are `NaN`: turn and stall need wings, hover drift needs a rotor.
40
+ 3. **Start from the real aircraft**: mass, wing area, span, tail arm, thrust. Inertia is roughly `m·(span/4)²` for roll and `m·(length/4)²` for pitch and yaw.
41
+ 4. **Trim it.** With the wing on the centre of mass, set wing `incidence` to the cruise lift coefficient over the lift slope, `W / (½ρV²·S·a)`, and leave the tail at `0`. It then flies level hands-off at that speed.
42
+ 5. **Drive it**: `bun run drive flight --key KeyR+KeyS:10500 --key KeyD:1500 --record <name> --record-fps 20`, or `bun run drive flight-heli --key KeyR:1500 --wait 3000 --key KeyE:3500 --record <name> --record-fps 20` for the helicopter, or `bun run drive flight-rocket --key KeyR:400 --wait 12000 --record <name> --record-fps 20` for the two-stage rocket.
43
+
44
+ ## Symptom → knob
45
+
46
+ | Symptom | Knob (direction) |
47
+ | --- | --- |
48
+ | Pitches up or down hands-off | wing `incidence` ↓ / ↑; wing further aft of the centre of mass ↑ stability |
49
+ | Loops too slowly | tail `control.pitch` ↑ (more AoA), entry speed or thrust ↑, `inertia.pitch` ↓ |
50
+ | Stalls in every hard pull | tail `control.pitch` ↓ so the elevator can't reach the stall angle, or wing `stallAngle` ↑ |
51
+ | Stall is a cliff / too gentle | `postStallLift` ↓ / ↑ |
52
+ | Rolls too slowly / too fast | aileron `control.roll` ↑ / ↓, wing panel `at` further out ↑ damping; `inertia.roll` sets how fast it gets there |
53
+ | Nose wanders, won't track | fin `area` ↑ or fin further aft |
54
+ | Controls feel laggy | `controls.<channel>.rate` ↑ |
55
+ | Top speed wrong | `engine.maxThrust`, `dragArea`, surface `cd0` |
56
+ | Bleeds speed in turns | `inducedDrag` ↓, wing `area` ↑ (lower CL for the same lift) |
57
+ | Throttle feels sluggish | `engine.spoolRate` ↑ |
58
+ | Helicopter won't hover / climbs away | hover collective is `weight / maxThrust` out of ground effect; `rotor.maxThrust` sets how much margin there is |
59
+ | Nose swings too hard without pedal | `rotor.torque` ↓, `inertia.yaw` ↑, `tail.sideDamping` ↑ |
60
+ | Pedal too weak to stop a swing | `tail.maxThrust` ↑ or tail rotor further aft |
61
+ | Cyclic twitchy / sluggish | `rotor.cyclic` ↓ / ↑, hub height (`rotor.at[1]`) ↓ / ↑, `rotor.damping` ↑ / ↓ |
62
+ | Pedal also rolls the body | tail rotor `at[1]` above the centre of mass; move it down or trim with cyclic |
63
+ | Rocket weathercocks too hard / tumbles | fin `area` ↓ / ↑, fins further aft for more stability |
64
+ | Gimbal too twitchy / can't steer | `motor.gimbal` ↓ / ↑; lighter late in the burn it turns faster on its own |
65
+ | Burns too long / short | `propellantKg` over `massFlow` sets burn time; the curve's last point cuts it off first |
66
+ | Helicopter too hard to hover on a keyboard | `assists.sas.yaw` 1 for heading hold, `assists.hoverHold` for cyclic; collective stays the pilot's |
67
+ | Assisted aircraft oscillates | `assists.sasRate` ↑ (softer), strengths ↓ |
68
+ | Players stall it pulling hard | `assists.maxAngleOfAttack` just under the wing `stallAngle`, or `maxG` |
69
+ | Sinks when it slows down | that is translational lift going away; `rotor.translationalLift` sets how much |
70
+
71
+ ## Traps
72
+
73
+ - `createAircraftDynamics` (`physics/flightDynamics`) is deprecated: it commands rotation and can't loop. The CHANGELOG Migrate note maps its fields onto `createRigidAircraft`.
74
+ - Setting orientation or angular velocity by hand defeats the model. Change a surface, the inertia or the control authority.
75
+ - A single centreline wing has no roll arm, so ailerons on it do nothing. Split it into left and right panels.
76
+ - For multiplayer, send inputs, not poses. Predict with the same tuning and `restore(serverState)` on correction.
@@ -0,0 +1,43 @@
1
+ # Recipe — NPC senses (perceive → remember → decide → route)
2
+
3
+ **What this wires:** an NPC that notices things, remembers where they were, acts on
4
+ that memory, and gives up when it goes stale. A guard that hears a sprint and
5
+ checks it out, a mob that leashes home, a pedestrian that flees a gunshot and a
6
+ patrol car that drives to the last sighting are all this loop with different
7
+ facts, graphs and speeds. No archetypes: the NPC is data plus a few named actions.
8
+
9
+ Working example: the `guard-probe` dev demo (`apps/dev/src/demo/guardProbe.ts`),
10
+ `bun run drive guard-probe`.
11
+
12
+ ## The seams
13
+
14
+ - **Perceive → memory.** One `createPerception` per group of NPCs that share
15
+ senses. Each tick, push the stimuli your game produces (`sound` with a
16
+ `loudness` that scales range, `damage`) and call `observe(npc, candidates, nowMs)`
17
+ with a *bounded* candidate list from a spatial query, never every entity. Pass
18
+ `occluded` for walls. Memory decays linearly over `memorySeconds`; each observer
19
+ judges a stimulus once.
20
+ - **Memory → blackboard.** A small system copies the facts the graph needs into
21
+ the NPC's persistent blackboard: `behaviorControl(ctx).blackboard(id)`. Write
22
+ plain scalars (`alerted`, `lastKnownX`, `lastKnownZ`, `confidence`). Actions
23
+ and conditions read facts; they do not query the world for them.
24
+ - **Blackboard → decision.** The NPC's `decisionGraph` behavior is data:
25
+ conditions on blackboard keys, `memory` sequences for multi-step errands
26
+ (go → `wait` → give up), `cooldown` for barks, `random` (drawn from `ctx.rng`)
27
+ for idle variety. Register actions by name with `registerBehaviorActions`, and
28
+ release movement or claims in `onAbort` when a higher branch pre-empts them.
29
+ Set `thinkInterval` so a crowd thinks a few times a second.
30
+ - **Decision → route.** Actions that move ask one `createNavMeshQuery(mesh)` per
31
+ mesh for `findPath` and replan only when the goal moves. Bake the mesh from
32
+ scene geometry with `@jgengine/navbake` (`await initNavBake()`, then
33
+ `bakeNavMesh`) and store it as an editor `nav` bake; price hazards or lock doors
34
+ with `areaCosts` through `retune`.
35
+
36
+ ## Gotchas
37
+
38
+ - Hand the graph facts, not objects: the blackboard serializes with the behavior,
39
+ so saves and replays keep an NPC mid-investigation.
40
+ - Judge footstep noise on speed averaged over a window, not one frame; frame-time
41
+ jitter otherwise reads as a sprint.
42
+ - Record the stimulus you handled (`handledAt`) so giving up does not
43
+ re-alert on the same memory next tick.
@@ -0,0 +1,85 @@
1
+ # Recipe — vehicle feel (feel target → metrics → physical knobs)
2
+
3
+ **What this wires:** a ground vehicle that feels the way the brief says, from sim to camera, sound and rumble. There is no default car: every vehicle is a set of physical numbers the game picks for its own feel, checked against metrics it writes down first.
4
+
5
+ ## The seams
6
+
7
+ - **Sim.** `createVehicleDynamics(tuning, { surfaceFriction?, clampMove?, groundHeight? })` from `@jgengine/core/physics/vehicleDynamics`. Yaw comes from tire forces, so balance, slides and recovery are outcomes of the numbers, not special cases. It has `snapshot()`/`restore()` for prediction and replay, `retune()` for upgrades and damage, and per-tick modifiers (`driveScale`, `gripScale`, `steerScale`, `brakeScale`, `thrust` for boost).
8
+ - **Terrain.** Add a `suspension` block (spring and damper rates, travel, ride height, anti-roll) and pass `groundHeight` for ramps, hills and jumps. Each wheel then samples the ground, the body heaves, pitches and rolls on real springs, slopes pull the car downhill, and the step reports `airborne`, `landingSpeed` and per-corner `wheelLoads`. Without the block the car stays on a flat plane.
9
+ - **Input.** Put `createAxisShaper` (`@jgengine/core/input/axisShaper`) between `ctx.input.axis(...)` and the sim. Keys get `riseRate`/`returnRate`, so a tap steers a little and a hold builds up; sticks get `deadzone`/`curve` with no lag. Pass `{ analog: analogAxes(bindings, ctx.input.analog()) }` each frame so each axis picks the right profile.
10
+ - **Pad feel.** Set the stick deadzone, curve and trigger deadzone once in `defineGame({ gamepad: { deadzone, curve, triggerDeadzone } })`. The shell applies it before any analog value reaches `ctx.input`, so per-axis shaping in `createAxisShaper` only adds ramps and speed scaling on top.
11
+ - **Jumps and air.** With `suspension`, a `jump` block (`speed`, `count` for double jumps, `window`) enables `car.jump()`, and an `air` block (`pitchAccel`/`yawAccel`/`rollAccel`, `damping`, `maxRate`) rotates the body in flight. Air input comes from `modifiers.air`; without it, throttle−brake pitches and steer yaws, which suits Rocket League-style play. Pass `{ pitch: 0, yaw: axis.steer, roll: 0 }` if W should keep driving instead of nosing down. `applyAngularImpulse` composes dodges and flips. The body stays near level, so full flips and wall-driving wait for the rigid-body backend.
12
+ - **Boats.** `createBoatDynamics` (`@jgengine/core/physics/boatDynamics`) takes the same axis input and returns a step `tickDrivableVehicle` and `measureHandling` accept. Its knobs are mass, length, beam, prop thrust and prop speed, `planingSpeed`, `keel` and `steering` (`rudder` with an area, or `outboard`). A tug and a speedboat differ only in those numbers.
13
+ - **Collision.** In a `PhysicsBackend` world (Rapier or `PhysicsWorld`), `createVehicleBackendLink` gives the car a kinematic chassis that shoves props, and a `clampMove` that stops it at walls. The sim keeps the handling, so the feel numbers above still hold.
14
+ - **Two wheels.** A `lean` block (`maxLean`, `leanRate`, optional `countersteer`) turns the same sim into a motorcycle or bicycle.
15
+ - Steer asks for a lean, and the bar steers the balanced turn that lean needs.
16
+ - The bike tips the other way briefly as the lean starts, which is countersteer.
17
+ - The step reports the balance lean `atan(lateral g)` as `lean`, with `bodyRoll = -lean`. The requested lean is capped at what grip can hold.
18
+ - `wheelie`/`stoppie` flags come from axle loads. `assists.antiWheelie` and `assists.antiStoppie` cap drive and braking just under the pitch-over limit, and on springs they also back off as the axle goes light. A sprung bike needs them to stay playable.
19
+ - `measureLean` reports steady lean, time to lean and counter-lean.
20
+ - **Pose.** `tickDrivableVehicle(car, dt, ctx.input.axis(bindings, ranges), { groundHeight })` returns a `setPose` patch. Pitch and roll come from load transfer, or from the springs when `suspension` is set.
21
+ - **Camera.** `camera: { rig: "chase", chase: { fov, lead, bank, velocityYaw, yawResponse } }`. `velocityYaw` shows the car's side in a slide, and `fov` widens with speed.
22
+ - Speed comes from the entity's sim `velocity`, so publish poses with `setPose({ ..., dt })` (`tickDrivableVehicle` does). `fov.response` eases the FOV.
23
+ - The boom holds its length at any speed. `distanceBySpeed: { extra }` pulls it back on purpose; `ctx.camera.kickFov(deg)` adds a decaying punch for landings and hits.
24
+ - `pitchFollow` tilts the boom with `rotationX` on ramps and slopes. `lookBackAction` names an input action that looks behind while held.
25
+ - The boom stops short of objects, terrain and walls through `ctx.scene.raycast`; `collision: false` turns that off.
26
+ - Switch views at runtime with `ctx.camera.setChaseTuning({ ...ctx.camera.chaseTuning(), view: nextChaseView(view) })` (`@jgengine/core/runtime/cameraDirector`).
27
+ - **Sound.** Call `ctx.game.audio.loop(id, sound)` once, then `setLoop` every tick:
28
+ - Engine: `rate` from `step.rpm`, `gain` from `step.engineLoad`.
29
+ - Tires: `gain` from how far `max(step.frontSaturation, step.rearSaturation)` exceeds ~0.85.
30
+ - Real engine samples: `createEngineLayers` (`@jgengine/core/audio/engineLayers`) takes N loops keyed by the rpm they were recorded at, crossfades the two around the current rpm, and picks on-load or off-load sets by `engineLoad`. Call `update(dt, { rpm, load })` then `play(ctx.game.audio, { at, velocity, gain, lowpass })` each tick.
31
+ - `setLoop` also takes `lowpass`/`highpass` cutoffs in Hz (muffle off-throttle) and `velocity`; a sound with `doppler: 1` pitches by listener and emitter velocity.
32
+ - **One place for all of it.** `createFeedbackMixer` (`@jgengine/core/vfx/feedbackMixer`) declares these mappings as routes: `{ signal, target, curve, attack, release }`, with `max` or `sum` combining and threshold `events` for one-shots like a landing thud. Update it with the step's telemetry each tick and apply its targets to `setLoop`, the chase camera and `haptics`, instead of hand-writing the glue.
33
+ - **Rumble.** Set continuous channels each tick on `ctx.input.haptics(userId)`: `set("road", { strong: rearSaturation, weak: frontSaturation })`, an `engine` hum from load and rpm, and `pulse("impact", { strong, weak, ms }, 1)` on a landing so it ducks the rest. The shell mixes them by priority onto that player's pad. `ctx.input.rumble` stays for a single fire-and-forget effect.
34
+ - **Sound.** Play a one-shot on `step.landingSpeed` (thud, suspension clunk).
35
+ - **Proof.** For jumps and air control, `measureAir` reports apex height and time, double-jump height, and air rotation rates.
36
+ - **Proof.** `measureHandling(() => createVehicleDynamics(tuning), options?)` from `@jgengine/core/physics/handlingProbe` returns deterministic numbers. Assert them in a test. For a sprung car, `measureRide` adds settle time, brake dive, roll per g and landing bounces.
37
+
38
+ ## Workflow
39
+
40
+ 1. **Write the feel as numbers before tuning.** Turn the brief into target ranges on the `HandlingReport` fields. Some examples:
41
+ - "Snappy" is `turnIn < 0.2`.
42
+ - "Planted" is `maxLateralG` close to the tire μ with `stepPeakSideslipDeg < 8`.
43
+ - "Drifty but safe" is `handbrakePeakSideslipDeg` 40–70 with `spun === false`.
44
+ - "Keyboard-friendly" is `spun` and `powerSteerSpun` both `false`, including at `cornerSpeed: 40`.
45
+ - "Neutral balance" is `measureCourse(...).understeerGradient` near 0–0.5 deg/g; "tail-happy" is below 0.
46
+ - "Agile" is a high `measureCourse(...).slalomSpeed`; "forgiving" is a short `handbrakeRecoverySeconds`.
47
+ 2. **Put those ranges in a test first**, next to the tuning.
48
+ 3. **Start from the real vehicle's physical numbers**: mass, wheelbase, weight split, CoM height, driven axle, torque and gears. Then move one knob at a time using the table below, rerunning the test after each change.
49
+ 4. **Drive it.**
50
+ - In the engine repo: `bun run drive <game> --key KeyW:4000 --probe a --key KeyW+KeyD:1500 --probe b --record <name>`. Chords hold throttle and steer together.
51
+ - `--record-fps 20` keeps game time at 1:1, because each recorded frame advances at most one 50 ms step.
52
+ - Tests prove the numbers; the drive proves the camera and sound.
53
+
54
+ ## Symptom → knob
55
+
56
+ | Symptom | Knob (direction) |
57
+ | --- | --- |
58
+ | `understeerGradient` too high | front tire `peakSlipAngle` ↓ or rear ↑ (tire stiffness split is what moves it at moderate g) |
59
+ | Pushes wide, won't turn in | `rollStiffnessFront` ↓, rear `peakGrip` ↓ relative to front, `yawInertiaIndex` ↓, `steering.rate` ↑ |
60
+ | Snaps into oversteer | `rollStiffnessFront` ↑, rear `peakGrip` ↑, `slideGrip` ↑ (gentler breakaway), `assists.stability` ↑ |
61
+ | Spins holding throttle and steer | `assists.tractionControl` ↑ (budgets for cornering grip), `driveFront` ↑, peak torque ↓ |
62
+ | Slides feel uncatchable | `steering.selfAlign` ↑ (caster catches the slide), `slideGrip` ↑, `assists.maxSideslip` ↑ with `stability` ↑ |
63
+ | Too twitchy at speed | `steering.highSpeedAngle` ↓, `steering.rate` ↓, `yawInertiaIndex` ↑, shaper steer `digital.riseRate` ↓ |
64
+ | Keyboard steering is all-or-nothing | shaper steer `digital.riseRate` 2–4/s, `returnRate` 5–8/s |
65
+ | Stick feels twitchy on centre | shaper steer `analog.curve` 1.3–2, `analog.deadzone` 0.05–0.12 |
66
+ | Sluggish, boat-like | `yawInertiaIndex` ↓, `peakSlipAngle` ↓ (stiffer tire), `comHeight` ↓ |
67
+ | Handbrake does nothing / spins every time | `handbrakeGrip` ↓ / ↑, with `selfAlign` to set how it recovers |
68
+ | No top speed ceiling / wrong ceiling | `aero.dragArea`, power (`maxPower` or torque × gearing); `speedLimit` only for a hard governor |
69
+ | Boost or supersonic above the cap | `modifiers.thrust` (bypasses tires and the governor) |
70
+ | Floaty, wallowy body | `suspension.springRate` ↑, `damperRate` ↑ |
71
+ | Harsh, skips over bumps | `springRate` ↓, `travel` ↑ |
72
+ | Landings bounce back into the air | `suspension.reboundRate` ↑ (defaults to 1.5 × `damperRate`), `travel` ↑ |
73
+ | Leans too much / feels flat | `suspension.antiRoll` ↑ / ↓; `rollStiffnessFront` still sets which axle takes it |
74
+ | Jump too floaty / too short | `jump.speed` (apex ≈ speed² / 2g); `jump.count: 2` plus `window` for a double jump |
75
+ | Air rotation too twitchy / sluggish | `air.*Accel` for how fast it builds, `air.maxRate` for the ceiling, `air.damping` for how fast it stops |
76
+ | Bike wheelies on launch / flips over the bars braking | `assists.antiWheelie` / `assists.antiStoppie` ↑ (1 keeps both wheels down; 0.6 lets it lift briefly) |
77
+ | Bike tips in lazily / nervously | `lean.leanRate` ↑ / ↓ (2 tourer … 5 sport); `lean.countersteer` sets the wrong-way tip at the start |
78
+ | Rolls over in corners | `comHeight` ↓ or `trackWidth` ↑: it tips over at about `trackWidth / (2 · comHeight)` g |
79
+
80
+ ## Traps
81
+
82
+ - Hand-editing `yawRate` or heading to "fix" a turn defeats the model. Change a physical knob instead.
83
+ - `speedLimit` fades drive out and never clamps velocity. A car pinned at its cap can still turn, and thrust can carry it past the cap.
84
+ - A slide that looks right on a gamepad can spin on a keyboard. Assert `powerSteerSpun` and `spun` with the default `stepSteer: 1`, which is a held key.
85
+ - For multiplayer, send inputs, not poses. Predict locally with the same tuning, and on a correction call `restore(serverState)` and replay the buffered inputs.