@jgengine/shell 0.26.0 → 0.27.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 (34) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/GamePlayerShell.d.ts +8 -0
  3. package/dist/GamePlayerShell.js +15 -1
  4. package/dist/environment/EnvironmentScene.js +51 -8
  5. package/dist/render/decalGeometry.d.ts +26 -0
  6. package/dist/render/decalGeometry.js +54 -0
  7. package/dist/render/decalTexture.d.ts +13 -0
  8. package/dist/render/decalTexture.js +114 -0
  9. package/dist/render/modelWind.d.ts +28 -0
  10. package/dist/render/modelWind.js +103 -0
  11. package/dist/render/useModelInstance.d.ts +1 -1
  12. package/dist/render/useModelInstance.js +3 -1
  13. package/dist/scatter/scatterModels.js +2 -0
  14. package/dist/scene/DecalRenderer.d.ts +2 -0
  15. package/dist/scene/DecalRenderer.js +74 -0
  16. package/dist/scene/GrassFieldRenderer.js +3 -2
  17. package/dist/scene/builtinSceneKindRenderers.js +2 -0
  18. package/dist/terrain/GrassCover.d.ts +21 -0
  19. package/dist/terrain/GrassCover.js +84 -0
  20. package/dist/terrain/grassCoverGeometry.d.ts +40 -0
  21. package/dist/terrain/grassCoverGeometry.js +112 -0
  22. package/dist/terrain/terrainDetailMaterial.d.ts +12 -0
  23. package/dist/terrain/terrainDetailMaterial.js +54 -1
  24. package/dist/terrain/terrainSurfaces.d.ts +6 -0
  25. package/dist/terrain/terrainSurfaces.js +10 -0
  26. package/dist/world/WorldScene.js +2 -1
  27. package/package.json +4 -4
  28. package/skills/jgengine/api.md +2 -2
  29. package/skills/jgengine-ui/api.md +32 -1
  30. package/skills/jgengine-ui/capabilities.md +4 -0
  31. package/skills/jgengine-world/SKILL.md +4 -0
  32. package/skills/jgengine-world/api.md +115 -5
  33. package/skills/jgengine-world/capabilities.md +20 -0
  34. package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
@@ -2565,6 +2565,15 @@
2565
2565
  - `SurfaceShape` (type): type SurfaceShape = "box" | "cylinder" — Primitive UV layout whose physical dimensions drive `repeatMetres`.
2566
2566
  - `useAuthoredSurfaceMaterial` (function): function useAuthoredSurfaceMaterial(config: AuthoredSurfaceConfig, shape: SurfaceShape = "box"): THREE.MeshStandardMaterial — Load declared surface-map roles and own the material/map clones (cached textures remain untouched). Colour, emissive, sheen-colour and specular-colour maps are sRGB; numeric maps are linear. Cylinder side metres use the RMS ellipse circumference approximation. Static batches and custom meshes share this lifecycle.
2567
2567
 
2568
+ ## @jgengine/shell/render/decalGeometry
2569
+
2570
+ - `DecalDrapeGeometry` (interface): interface DecalDrapeGeometry — Baked vertex/index buffers for one draped ground decal.
2571
+ - `DecalDrapeOptions` (interface): interface DecalDrapeOptions — Inputs for {@link createDecalDrapeGeometry}.
2572
+
2573
+ ## @jgengine/shell/render/decalTexture
2574
+
2575
+ - `DecalTextureData` (interface): interface DecalTextureData — Row-major RGBA bytes plus their edge length, ready for a `THREE.DataTexture`.
2576
+
2568
2577
  ## @jgengine/shell/render/materialAppearanceSignals
2569
2578
 
2570
2579
  - `RenderMaterialAppearanceBaseline` (interface): interface RenderMaterialAppearanceBaseline extends MaterialAppearanceBaseline — Immutable resolved baseline for one caller-owned assigned material, including its original card exposure.
@@ -2624,6 +2633,14 @@
2624
2633
  - `cloneModelScene` (function): function cloneModelScene(source: THREE.Object3D, options?: { cloneMaterials?: boolean; shadows?: ModelShadowMode; surfaceDetail?: boolean }): THREE.Object3D — Clone a model with independent pose and materials, retaining shared geometry and textures.
2625
2634
  - `disposeModelScene` (function): function disposeModelScene(root: THREE.Object3D): void — Release materials and bone textures owned by `cloneModelScene`; shared assets and attached models remain owned by their callers.
2626
2635
 
2636
+ ## @jgengine/shell/render/modelWind
2637
+
2638
+ - `ResolvedModelWind` (interface): interface ResolvedModelWind — Resolved sway settings baked into one material.
2639
+ - `WindClock` (function): function WindClock(): null — Advances the shared gust clock once per frame; mount one per 3D world.
2640
+ - `applyModelWind` (function): function applyModelWind(material: THREE.MeshStandardMaterial, wind: ResolvedModelWind): THREE.MeshStandardMaterial — Compiles height-ramped wind sway into a standard material's vertex shader. Idempotent per material; the clock and direction are shared module uniforms, so the whole scene advances with one write per frame and no per-material bookkeeping.
2641
+ - `windClock` (const): const windClock: { value: number; } — Shared gust clock every foliage material reads; advanced once per frame by `WindClock`.
2642
+ - `windDirection` (const): const windDirection: Vector2 — Shared gust direction (XZ); a model may override it with its own authored direction.
2643
+
2627
2644
  ## @jgengine/shell/render/projectedOverdraw
2628
2645
 
2629
2646
  - `ProjectedOverdrawOptions` (interface): interface ProjectedOverdrawOptions — Sampling resolution; lower resolutions change thin geometry and alpha-mask raster coverage.
@@ -2695,7 +2712,7 @@
2695
2712
  ## @jgengine/shell/render/useModelInstance
2696
2713
 
2697
2714
  - `ModelInstance` (interface): interface ModelInstance — Render `scene` with `position` and uniform `scale`; animate or deform `content` inside that placement frame.
2698
- - `ModelInstanceConfig` (type): type ModelInstanceConfig = Pick<ModelConfig, "url" | "scale" | "targetHeight" | "y" | "anchor" | "dims" | "shadows" | "animation" | "hiddenNodes" | "surfaceDetail" > — Model loading, placement, shadows and animation for a custom renderer.
2715
+ - `ModelInstanceConfig` (type): type ModelInstanceConfig = Pick<ModelConfig, "url" | "scale" | "targetHeight" | "y" | "anchor" | "dims" | "shadows" | "animation" | "hiddenNodes" | "surfaceDetail" | "wind" > — Model loading, placement, shadows and animation for a custom renderer.
2699
2716
  - `ModelInstanceOptions` (interface): interface ModelInstanceOptions — Caller-owned styling runs on the isolated clone, never the loader cache.
2700
2717
  - `useModelInstance` (function): function useModelInstance(model: ModelInstanceConfig, options: ModelInstanceOptions = {}): ModelInstance — Load and own an isolated model instance, preserving imported transforms and measuring placement at bind pose. Apply hiddenNodes to the configured clone before measurement; equivalent name lists retain the instance. Mount `scene` with the returned position/scale. Custom materials, deformation and surrounding geometry stay caller-owned. Must run inside an R3F canvas and Suspense boundary; a game provider is optional.
2701
2718
 
@@ -3029,6 +3046,11 @@
3029
3046
  - `EditableGround` (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element — ⚠ undocumented
3030
3047
  - `EditableGroundProps` (interface): interface EditableGroundProps { terrain: Pick<EditableTerrain, "sampleHeight" | "surfaceAt">; bounds: Aabb; segments?: number; version?: number; baseColor?: string; surfaceColors?: Record<string, string> } — ⚠ undocumented
3031
3048
 
3049
+ ## @jgengine/shell/terrain/GrassCover
3050
+
3051
+ - `GrassCover` (function): function GrassCover({ area, seed, colorBase, colorTip, strength, edgeFeather, heightAt, coverageAt, segments, position, }: GrassCoverProps): React.JSX.Element | null — The turf surface under one authored `grass_field` patch: a ground-draped, edge-feathered mat that turns a bare pavement footprint into grass the blade field stands in. Bounded to one draw and a {@link GRASS_COVER_MAX_SEGMENTS}² grid, so a large park still costs a single mesh.
3052
+ - `GrassCoverProps` (interface): interface GrassCoverProps — Props for {@link GrassCover}: one authored patch's footprint, colours and drape samplers.
3053
+
3032
3054
  ## @jgengine/shell/terrain/GrassField
3033
3055
 
3034
3056
  - `DEFAULT_GRASS_COUNT` (const): const DEFAULT_GRASS_COUNT: 1500 — ⚠ undocumented
@@ -3052,6 +3074,13 @@
3052
3074
  - `DEFAULT_GRASS_COUNT` (const): const DEFAULT_GRASS_COUNT: 1500 — ⚠ undocumented
3053
3075
  - `DEFAULT_GRASS_DENSITY` (const): const DEFAULT_GRASS_DENSITY: 4 — Blades per square meter — matches `@jgengine/core/world/vegetation`'s `VEGETATION_DEFAULTS.density`, so an editor-placed grass volume and a bare `<GrassField>` agree on what "4" means.
3054
3076
 
3077
+ ## @jgengine/shell/terrain/grassCoverGeometry
3078
+
3079
+ - `GRASS_COVER_MAX_SEGMENTS` (const): const GRASS_COVER_MAX_SEGMENTS: 96 — Largest grid a cover mat bakes to, per axis — a 66 m park stays one draw at ~1 m cells.
3080
+ - `GRASS_COVER_TILE_METRES` (const): const GRASS_COVER_TILE_METRES: 3 — World metres one cover-texture tile spans; the mat's own grain is sampled in world space.
3081
+ - `GrassCoverGeometry` (interface): interface GrassCoverGeometry — Bake result for {@link createGrassCoverGeometry}; empty arrays when the mat is switched off.
3082
+ - `GrassCoverGeometryOptions` (interface): interface GrassCoverGeometryOptions — Inputs for {@link createGrassCoverGeometry}.
3083
+
3055
3084
  ## @jgengine/shell/terrain/grassGeometry
3056
3085
 
3057
3086
  - `GRASS_TUFT_BLADES` (const): const GRASS_TUFT_BLADES: 5 — Blades baked into each tuft instance — one instance reads as a clump, not a lone blade.
@@ -3108,9 +3137,11 @@
3108
3137
  ## @jgengine/shell/terrain/terrainDetailMaterial
3109
3138
 
3110
3139
  - `ALL_TERRAIN_SURFACE_CLASSES` (const): const ALL_TERRAIN_SURFACE_CLASSES: 511 — Compile every surface response, for an editor viewport whose paint can change to any surface.
3140
+ - `MAX_TERRAIN_LAYER_MAPS` (const): const MAX_TERRAIN_LAYER_MAPS: 4 — At most this many painted layers sample their own scanned map; extra layers keep procedural shading.
3111
3141
  - `TerrainDetailMaterialHandle` (interface): interface TerrainDetailMaterialHandle — The built procedural detail terrain material, ready to mount on the ground mesh.
3112
3142
  - `TerrainDetailMaterialOptions` (interface): interface TerrainDetailMaterialOptions — Surface-pass inputs beyond the resolved detail config.
3113
3143
  - `TerrainDetailMaterialTextures` (interface): interface TerrainDetailMaterialTextures — Loaded PBR textures matching a resolved `ResolvedTerrainDetailMaterial.maps`' roles. The caller (a React component, via `useTexture`/`useLoader`) owns loading and disposal; `core` never touches `THREE.Texture` and this shell function never fetches a URL itself. Omitted AO/roughness skip sampling.
3144
+ - `TerrainLayerMap` (interface): interface TerrainLayerMap — One painted layer's scanned color map plus the world metres one tile covers.
3114
3145
 
3115
3146
  ## @jgengine/shell/terrain/terrainMath
3116
3147
 
@@ -508,6 +508,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
508
508
 
509
509
  - `modelLoadStarts` (function) · `import { modelLoadStarts } from "@jgengine/shell/render/modelLoad"`
510
510
 
511
+ ## model-wind — sway a foliage model in the shared gust field
512
+
513
+ - `applyModelWind` (function) · `import { applyModelWind } from "@jgengine/shell/render/modelWind"`
514
+
511
515
  ## multiplayer-p2p — bootstrap a playable authoritative world for browser peer co-op
512
516
 
513
517
  - `resolvePeerShellMultiplayer` (function) · `import { resolvePeerShellMultiplayer } from "@jgengine/shell/multiplayer"`
@@ -63,6 +63,10 @@ Route on an editor-baked polygon nav mesh through `createNavRuntime` (`nav/navRu
63
63
 
64
64
  Move many agents across that mesh with `createNavCrowd` (`nav/navCrowd`): it plans corridors, follows the funnel-smoothed waypoints, keeps them apart with `createRvoCrowd` (`ai/rvo`) reciprocal velocity obstacles, bounds path searches to a per-tick time and count budget, and serializes through `snapshot`/`restore`. Give a crowd converging on one spot a `spread` radius so agents fan out (`ai/crowd`'s stable per-id offset) instead of stacking. Avoidance keeps agents inside the mesh's outer bounds; interior walls remain the game's collision layer.
65
65
 
66
+ ### Street traffic
67
+
68
+ Vehicles on roads do not path-find: they follow authored lane spans. `createLaneGraph` (`nav/laneGraph`) resolves those spans — `traffic_lane` paths, or `road` paths from the editor's street bake, split where streets cross — into junctions, crossings with a signal program or first-come right-of-way, and parking spots; `laneGraphFromDocument` reads that straight off the scene document, so editing a road moves its traffic. `createTraffic` (`nav/traffic`) then drives cars on it: IDM car following and braking, one car at a time through each intersection box (only once the exit lane has room, which is what keeps a blocked crossing from wedging the network), signals with phases, lane changes across authored `left`/`right` neighbours, parking that reserves a spot, and `setDemand(count)` for fleet size — the time-of-day curve is the caller's, not the engine's. Cars are posed, not driven: when a player or a chase needs real handling, feed intent into `physics/kinematicVehicle` and let this seam keep the ambient city.
69
+
66
70
  ### Fallback-seam diagnostics
67
71
 
68
72
  Render seams fall back to placeholders when content is unauthored: default green ground (no environment), primitive capsule/box actors (no `entityModels`/`objectModels` mapping), stylized proxy foliage (no scatter `resolveItem`). These placeholders are sometimes intended, so runtime never changes — but a dev-only, opt-in probe (`@jgengine/core/devtools/fallbackSeams`) tells "unauthored/misconfigured" apart from "intended". The shell arms it via `armFallbackSeams(devtoolsEnabled)`; each seam reports at its choice site (`reportFallbackSeam`), and counts surface through the devtools `fallbacks` probe. Disarmed (production) it is a pure, allocation-free no-op.
@@ -908,6 +908,23 @@
908
908
  - `CorridorSample` (interface): interface CorridorSample { distance: number; closest: CorridorPoint; edgeIndex: number } — ⚠ undocumented
909
909
  - `createCorridorField` (function): function createCorridorField(edges: readonly CorridorEdge[]): CorridorField — ⚠ undocumented
910
910
 
911
+ ## @jgengine/core/nav/laneGraph
912
+
913
+ - `LaneEdgeConfig` (interface): interface LaneEdgeConfig — One directed lane span to resolve into the graph.
914
+ - `LaneGraph` (interface): interface LaneGraph — Authored lane topology: junctions, directed spans with speed limits and arc lengths, crossings with their right-of-way, and parking spots. Positions come from the spans themselves, so a traffic runtime never re-derives geometry or runs a path search per vehicle.
915
+ - `LaneGraphConfig` (interface): interface LaneGraphConfig — The authored lane graph: junctions, directed spans, crossings and parking spots.
916
+ - `LaneGraphDocumentOptions` (interface): interface LaneGraphDocumentOptions — How the document reader recognizes authored lane content.
917
+ - `LaneIntersection` (interface): interface LaneIntersection — A resolved crossing: the lanes that feed it, the lanes that leave it, and its right-of-way.
918
+ - `LaneIntersectionConfig` (interface): interface LaneIntersectionConfig — Right-of-way and signal timing for one authored crossing.
919
+ - `LaneNode` (interface): interface LaneNode — A junction where lane spans meet.
920
+ - `LanePose` (interface): interface LanePose — World pose of a lane at an arc position.
921
+ - `LaneRightOfWay` (type): type LaneRightOfWay = "signal" | "priority" — How an intersection resolves which approach may enter next.
922
+ - `LaneSignalProgram` (interface): interface LaneSignalProgram — Signal timing for one intersection; phases list the in-lanes each green window serves.
923
+ - `LaneSpan` (interface): interface LaneSpan — A resolved lane span: centerline, arc lengths, limit and adjacency.
924
+ - `LaneSpot` (interface): interface LaneSpot — A parking spot projected onto one lane at an arc position.
925
+ - `createLaneGraph` (function): function createLaneGraph(config: LaneGraphConfig): LaneGraph — Read handle over the graph resolved once at construction. The build mutates its working set; this object never changes, so a save system rebuilds it from the scene document instead of serializing it.
926
+ - `laneGraphFromDocument` (function): function laneGraphFromDocument(doc: Pick<EditorDocument, "paths" | "markers">, options: LaneGraphDocumentOptions = {}): LaneGraph — Read a lane graph out of an authored scene document. `traffic_lane` paths are one directed lane each; `road` paths (the editor's street bake) become a lane per direction, offset onto their own side of the road; `traffic_intersection` markers carry a signal program and right-of-way in `meta`; `traffic_parking` markers place parking spots. Runtime positions come from the paths, so editing a road moves its traffic instead of leaving a duplicate lane behind.
927
+
911
928
  ## @jgengine/core/nav/navConstrain
912
929
 
913
930
  - `NavConstrainEntity` (interface): interface NavConstrainEntity { position: EntityPosition } — ⚠ undocumented
@@ -1017,6 +1034,18 @@
1017
1034
  - `TimetableStop` (interface): interface TimetableStop { at: Waypoint; dwellSeconds?: number } — ⚠ undocumented
1018
1035
  - `createRouteTimetable` (function): function createRouteTimetable(config: RouteTimetableConfig): RouteTimetable — ⚠ undocumented
1019
1036
 
1037
+ ## @jgengine/core/nav/traffic
1038
+
1039
+ - `Traffic` (interface): interface Traffic — Traffic on an authored lane graph: cars follow lane spans with IDM car-following, hold at stop lines, cross intersections under a signal phase or first-come right-of-way, change lanes, park in authored spots, and grow or shrink with the caller's `demand`. Deterministic — cars advance in insertion order, ties break on id, and the injected generator is the only source of variety. Bounded per tick: one pass over the cars, one sorted pass per occupied lane, no path search.
1040
+ - `TrafficCarView` (interface): interface TrafficCarView — One car's live state, as a renderer, a game or the devtools overlay reads it.
1041
+ - `TrafficOptions` (interface): interface TrafficOptions — Everything {@link createTraffic} needs: the graph, the injected generator and the caller's policies.
1042
+ - `TrafficRoutePolicy` (type): type TrafficRoutePolicy = (context: { carId: string; laneId: string; nodeId: string; /** Outbound lanes that are not the lane the car arrived on, sorted by id. */ options: readonly string[]; }) => string | null — Caller-supplied exit choice at a junction: return one of `options`, or `null` to leave the graph.
1043
+ - `TrafficSignalState` (interface): interface TrafficSignalState — An intersection's signal right now: which phase is green and for how long.
1044
+ - `TrafficSnapshot` (interface): interface TrafficSnapshot — Serializable traffic state: the fleet, the parking occupancy, the signal clocks and the demand.
1045
+ - `TrafficStats` (interface): interface TrafficStats — Counters a devtools probe, a HUD or a test reads.
1046
+ - `TrafficTuning` (interface): interface TrafficTuning — Every per-car behaviour number, retunable in play.
1047
+ - `createTraffic` (function): function createTraffic(options: TrafficOptions): Traffic — Traffic for an authored lane graph (see {@link createLaneGraph}).
1048
+
1020
1049
  ## @jgengine/core/orders/orderKinds
1021
1050
 
1022
1051
  - `AttackMoveOrderPayload` (interface): interface AttackMoveOrderPayload — Payload for an attack-move: advance toward `point`, engaging hostiles encountered en route.
@@ -1218,8 +1247,9 @@
1218
1247
  - `KinematicVehicleModifiers` (interface): interface KinematicVehicleModifiers — Per-tick multipliers layered over the base tuning — the transient overrides games apply for one frame without rebuilding the vehicle: nitro/boost, a braced-plow bonus, or entering a speed zone or slow field. Each defaults to `1` (no change), so passing nothing leaves the base tuning untouched.
1219
1248
  - `KinematicVehicleOptions` (interface): interface KinematicVehicleOptions { position?: readonly [number, number, number]; heading?: number; surfaceFriction?: (x: number, z: number) => number; dragAt?: (x: number, z: number) => number; clampMove?: (from: readonly [number, number], to: readonly [number, number]) =>… — ⚠ undocumented · used by `createKinematicVehicle`: Arcade ground-vehicle sim (car, kart, bike) with throttle, brake, steer, drift and retunable handling.
1220
1249
  - `KinematicVehicleState` (interface): interface KinematicVehicleState — Serializable integrator state of a {@link KinematicVehicle}: what `restore` needs to resume bit-for-bit.
1221
- - `KinematicVehicleStep` (interface): interface KinematicVehicleStep { position: readonly [number, number, number]; heading: number; forwardSpeed: number; lateralSpeed: number; slip: number; surface: number; gear: number; rpm: number; steerAngle: number; yawRate: number; longitudinalAcceleration: number; tra… — ⚠ undocumented · used by `tickDrivableVehicle` (@jgengine/core/physics/drivableVehicle): Connects an `AxisInput` sample straight through a ground-vehicle sim (`KinematicVehicle` or `VehicleDynamics`) to a scene entity's pose for …
1250
+ - `KinematicVehicleStep` (interface): interface KinematicVehicleStep { position: readonly [number, number, number]; heading: number; forwardSpeed: number; lateralSpeed: number; slip: number; surface: number; weightTransfer: number; loadGrip: number; gear: number; rpm: number; steerAngle: number; yawRate: num… — ⚠ undocumented · used by `tickDrivableVehicle` (@jgengine/core/physics/drivableVehicle): Connects an `AxisInput` sample straight through a ground-vehicle sim (`KinematicVehicle` or `VehicleDynamics`) to a scene entity's pose for …
1222
1251
  - `KinematicVehicleTuning` (interface): interface KinematicVehicleTuning { engineAccel: number; brakeAccel: number; topSpeed: number; reverseSpeed: number; turnRate: number; turnSpeedRef: number; grip?: GripCurve; gripStrength: number; handbrakeGrip: number; rollingResistance?: number; coastDeceleration?: number… — ⚠ undocumented · used by `createKinematicVehicle`: Arcade ground-vehicle sim (car, kart, bike) with throttle, brake, steer, drift and retunable handling.
1252
+ - `KinematicWeightTransferTuning` (interface): interface KinematicWeightTransferTuning — Load-transfer handling for the arcade model. Weight moves under braking, traction and cornering; the tire that goes light loses more grip than the loaded one gains (load sensitivity), so the axle's combined grip falls. Every term is bounded by the geometry, and `sensitivity` caps the loss, so the transfer can never drive grip negative or diverge. For per-axle balance from tire forces, use `createVehicleDynamics` (`physics/vehicleDynamics`) instead.
1223
1253
  - `createKinematicVehicle` (function): function createKinematicVehicle(initialTuning: KinematicVehicleTuning, options: KinematicVehicleOptions = {}): KinematicVehicle — Arcade ground-vehicle sim (car, kart, bike) with throttle, brake, steer, drift and retunable handling.
1224
1254
 
1225
1255
  ## @jgengine/core/physics/physicsBackend
@@ -1339,6 +1369,21 @@
1339
1369
  - `createVehicleBody` (function): function createVehicleBody(world: PhysicsWorld, config: VehicleBodyConfig): VehicleBody — ⚠ undocumented
1340
1370
  - `sampleGripCurve` (function): function sampleGripCurve(curve: GripCurve, slip: number): number — Piecewise-linear tire-grip curve: normalized lateral slip → available grip (0..1). Grip peaks near the breakaway slip then falls off as the tire slides — the shape that separates a planted corner from a drift. Points are read in ascending slip order; ends clamp.
1341
1371
 
1372
+ ## @jgengine/core/physics/vehicleDamage
1373
+
1374
+ - `VehicleDamage` (interface): interface VehicleDamage — Fixed-zone crash damage for a vehicle: impacts accumulate into discrete stages, stages change handling and show a bounded dent, and a part can come off at a set stage. Data-driven and bounded by construction — the zone set is authored, each zone's deformation is one capped displacement, and no mesh is edited. Serializes through {@link snapshot}/{@link restore}, so a save, a replay or a host correction resumes mid-crash.
1375
+ - `VehicleDamageConfig` (interface): interface VehicleDamageConfig — Everything {@link createVehicleDamage} needs.
1376
+ - `VehicleDamageEffect` (interface): interface VehicleDamageEffect — Handling change one damage zone applies at its *last* stage. Multiplier fields read `1` = undamaged, and a zone part-way through its stages gets the fraction between; `steerPull` and `drag` are additive and reach their full value only at the last stage.
1377
+ - `VehicleDamageEffectResolved` (interface): interface VehicleDamageEffectResolved — Every zone's handling cost summed into one set of numbers (all fields present, defaults filled).
1378
+ - `VehicleDamageState` (interface): interface VehicleDamageState — Serializable damage state: {@link VehicleDamage.restore} needs only this.
1379
+ - `VehicleDamageTransition` (interface): interface VehicleDamageTransition — A zone crossing a threshold this tick — the event a HUD feed, a smoke puff or a camera shake keys off.
1380
+ - `VehicleDamageZone` (interface): interface VehicleDamageZone — One damageable zone: a fixed local body point, the impulse thresholds that step it through its stages, the bounded dent it shows, and the handling it costs. There is no mesh editing — a zone's deformation is one displacement it can never exceed, so the visual can never grow without limit.
1381
+ - `VehicleDamageZoneState` (interface): interface VehicleDamageZoneState — Live state of one zone, as a HUD or a save reads it.
1382
+ - `VehicleDeformation` (interface): interface VehicleDeformation — A zone's current dent: a bounded local displacement and whether the part has come off.
1383
+ - `createVehicleDamage` (function): function createVehicleDamage(config: VehicleDamageConfig): VehicleDamage — Fixed-zone crash damage (issue #86's stage model) wired to handling and visuals: see {@link VehicleDamage}.
1384
+ - `damagedAxis` (function): function damagedAxis(axis: AxisInput, effect: VehicleDamageEffectResolved): AxisInput — Apply a damage `steerPull` to a steer axis, clamped to the input range — a bent frame pulling one way.
1385
+ - `damagedTuning` (function): function damagedTuning(base: KinematicVehicleTuning, effect: VehicleDamageEffectResolved): KinematicVehicleTuning — Bake a damage effect into a tuning — the lasting-damage path. Hand the result to the sim's `retune`, which keeps the vehicle's momentum; the base tuning is not mutated, so a repair is the same call with the undamaged base.
1386
+
1342
1387
  ## @jgengine/core/physics/vehicleDynamics
1343
1388
 
1344
1389
  - `VehicleAeroTuning` (interface): interface VehicleAeroTuning — Aero and resistance. Uses air density 1.225 kg/m³.
@@ -1366,6 +1411,17 @@
1366
1411
  - `VehicleObstacleClamp` (interface): interface VehicleObstacleClamp — A planar move clamp for a kinematic car: feed {@link clampMove} to a vehicle's `clampMove` hook so an attempted XZ displacement slides along world solids instead of driving through them, and read {@link takeImpact} once per tick for the crash it produced.
1367
1412
  - `createVehicleObstacleClamp` (function): function createVehicleObstacleClamp(options: { /** Solids near the car this tick (already filtered to the relevant, solid set). */ obstacles: () => readonly CollisionObstacle[]; /** Vehicle body radius (units). Default {@link DEFAULT_VEHICLE_RADIUS}. */ radius?: number; /** Current tick dt (seconds)… — Build a slide-along move clamp for a kinematic car (#1051). `obstacles` is sampled fresh each tick — the caller hands back the already-filtered set of solids near the car — and `dt` supplies the current tick length so a blocked move's lost displacement converts to a closing speed. `radius` inflates each obstacle footprint by the car's body radius (default {@link DEFAULT_VEHICLE_RADIUS}).
1368
1413
 
1414
+ ## @jgengine/core/physics/vehicleParts
1415
+
1416
+ - `VehicleFeel` (interface): interface VehicleFeel — Comparable handling numbers derived from a tuning — what "the numbers and the feel" means to a shop screen, a HUD or a test. Grip is reported as the curve's own shape, so a compound change shows up as a moved peak rather than a single scalar.
1417
+ - `VehiclePart` (interface): interface VehiclePart — One installable part. `slot` is the caller's own grouping ("tires", "engine", "front-bumper") — the engine does not name slots, so a game's upgrade tree stays its own. Effects compose in install order, and a part's `gripCurve` replaces the base tire compound outright (last one installed wins).
1418
+ - `VehiclePartEffect` (interface): interface VehiclePartEffect — How one part moves a stat: multiply the base by `scale`, then add `add`. Both default to no change.
1419
+ - `VehicleSetup` (interface): interface VehicleSetup — A composed vehicle: the tuning to hand the sim, the feel it produces, and what was installed.
1420
+ - `VehicleStat` (type): type VehicleStat = | "engineAccel" | "brakeAccel" | "topSpeed" | "reverseSpeed" | "turnRate" | "turnSpeedRef" | "gripStrength" | "handbrakeGrip" | "rollingResistance" | "coastDeceleration" — A tunable vehicle number a part can move; each names a {@link KinematicVehicleTuning} field.
1421
+ - `applyVehicleParts` (function): function applyVehicleParts(base: KinematicVehicleTuning, parts: readonly VehiclePart[]): VehicleSetup — Fold parts onto a base tuning: every effect scales then adds onto the running value, in array order, so a setup is deterministic and a caller can reason about which part moved what. The base tuning is never mutated, so the same base can seed several builds.
1422
+ - `feelOf` (function): function feelOf(tuning: KinematicVehicleTuning): VehicleFeel — Read the handling numbers out of any tuning — base or parts-composed.
1423
+ - `withPart` (function): function withPart(parts: readonly VehiclePart[], next: VehiclePart): VehiclePart[] — Swap a part into its slot: any part already in `next.slot` is removed and `next` takes its place in the same install position, so the order (and therefore the composed numbers) stays stable across an upgrade.
1424
+
1369
1425
  ## @jgengine/core/physics/worldColliders
1370
1426
 
1371
1427
  - `WorldColliderSync` (interface): interface WorldColliderSync — Handle for synchronizing authored static world collision with a physics backend.
@@ -2332,7 +2388,7 @@
2332
2388
  - `GeneratedPart` (interface): interface GeneratedPart — One generated primitive part — a box/panel placed in the asset's local space.
2333
2389
  - `Glide` (class): class Glide — A reduced-gravity, forward-thrust glide over a physics body — wingsuit / glider / paraglider (Enshrouded, Grounded). Call `apply(dt, steerX, steerZ)` each frame *before* `world.step`: it feeds back most of the gravity the sim is about to apply (leaving `gravityScale` of it), pushes the body along the steer vector by `thrust`, and clamps descent to `maxFallSpeed`. Stop calling it to fall normally again — no attach/detach state to leak.
2334
2390
  - `Grapple` (class): class Grapple — A fired-anchor rope on the joint API — grapple (reel toward a hit point), zipline (rigid cable to a far anchor you then slide/reel along), swing (rigid rope + gravity = a pendulum). `fire` attaches a `distance`/`spring` joint from the traveller body to a fixed world point; `reel` shrinks its rest length so the constraint drags the body in; `moveAnchor` re-points it (zipline glide, grapple-to- moving-target). The pick — a raycast to find the anchor — is the caller's; core owns the constraint.
2335
- - `GrassEnvironmentDescriptor` (type): type GrassEnvironmentDescriptor = { kind: "grass" } & Required< Pick<GrassEnvironmentConfig, "area" | "density" | "bladeHeight" | "bladeWidth" | "windStrength" | "colors"> > & Pick<GrassEnvironmentConfig, "seed" | "bladeBend" | "tuftRadius" | "colorVariation" | "normalLift"> — ⚠ undocumented · used by `grass`: Declares a grass vegetation patch for `environment()` — area, blade sizing, density, and colors.
2391
+ - `GrassEnvironmentDescriptor` (type): type GrassEnvironmentDescriptor = { kind: "grass" } & Required< Pick<GrassEnvironmentConfig, "area" | "density" | "bladeHeight" | "bladeWidth" | "windStrength" | "colors"> > & Pick<GrassEnvironmentConfig, "seed" | "bladeBend" | "tuftRadius" | "colorVariation" | "normalLift" | "groundCover"> — ⚠ undocumented · used by `grass`: Declares a grass vegetation patch for `environment()` — area, blade sizing, density, and colors.
2336
2392
  - `GravityField` (interface): interface GravityField — Position-dependent gravity source used by movement and vehicle simulations.
2337
2393
  - `GravityVector` (type): type GravityVector = readonly [number, number, number] — Three-dimensional acceleration vector sampled from a gravity field.
2338
2394
  - `GripCurve` (interface): interface GripCurve { points: readonly (readonly [number, number])[] } — ⚠ undocumented · used by `sampleGripCurve`: Piecewise-linear tire-grip curve: normalized lateral slip → available grip (0..1).
@@ -2369,10 +2425,23 @@
2369
2425
  - `KinematicPowertrainTuning` (interface): interface KinematicPowertrainTuning — Data-first gearbox and torque-curve tuning for a kinematic ground vehicle.
2370
2426
  - `KinematicSteeringTuning` (interface): interface KinematicSteeringTuning — Bicycle-model steering settings; all angles are radians.
2371
2427
  - `KinematicVehicle` (interface): interface KinematicVehicle — The pure-kinematic arcade car every racing game hand-rolled (#282.1): steer-yaw scaled by speed, throttle/brake acceleration, and a grip-curve lateral-slip bleed — no `PhysicsWorld`, no wheels, just the drift-friendly integration the three shipped racers proved out. Games keep their flavor (drift meters, boost, off-track rules) via `surfaceFriction`/`dragAt` hooks and the returned slip.
2372
- - `KinematicVehicleStep` (interface): interface KinematicVehicleStep { position: readonly [number, number, number]; heading: number; forwardSpeed: number; lateralSpeed: number; slip: number; surface: number; gear: number; rpm: number; steerAngle: number; yawRate: number; longitudinalAcceleration: number; tra… — ⚠ undocumented · used by `tickDrivableVehicle`: Connects an `AxisInput` sample straight through a ground-vehicle sim (`KinematicVehicle` or `VehicleDynamics`) to a scene entity's pose for …
2373
- - `KinematicVehicleTuning` (interface): interface KinematicVehicleTuning { engineAccel: number; brakeAccel: number; topSpeed: number; reverseSpeed: number; turnRate: number; turnSpeedRef: number; grip?: GripCurve; gripStrength: number; handbrakeGrip: number; rollingResistance?: number; coastDeceleration?: number… — ⚠ undocumented · used by `createKinematicVehicle`: Arcade ground-vehicle sim (car, kart, bike) with throttle, brake, steer, drift and retunable handling.
2428
+ - `KinematicVehicleStep` (interface): interface KinematicVehicleStep { position: readonly [number, number, number]; heading: number; forwardSpeed: number; lateralSpeed: number; slip: number; surface: number; weightTransfer: number; loadGrip: number; gear: number; rpm: number; steerAngle: number; yawRate: num… — ⚠ undocumented · used by `tickDrivableVehicle`: Connects an `AxisInput` sample straight through a ground-vehicle sim (`KinematicVehicle` or `VehicleDynamics`) to a scene entity's pose for …
2429
+ - `KinematicVehicleTuning` (interface): interface KinematicVehicleTuning { engineAccel: number; brakeAccel: number; topSpeed: number; reverseSpeed: number; turnRate: number; turnSpeedRef: number; grip?: GripCurve; gripStrength: number; handbrakeGrip: number; rollingResistance?: number; coastDeceleration?: number… — ⚠ undocumented · used by `applyVehicleParts`: Fold parts onto a base tuning: every effect scales then adds onto the running value, in array order, so a setup is deterministic and a calle…
2430
+ - `KinematicWeightTransferTuning` (interface): interface KinematicWeightTransferTuning — Load-transfer handling for the arcade model. Weight moves under braking, traction and cornering; the tire that goes light loses more grip than the loaded one gains (load sensitivity), so the axle's combined grip falls. Every term is bounded by the geometry, and `sensitivity` caps the loss, so the transfer can never drive grip negative or diverge. For per-axle balance from tire forces, use `createVehicleDynamics` (`physics/vehicleDynamics`) instead.
2374
2431
  - `LANDMARK_HARD_CAP` (const): const LANDMARK_HARD_CAP: 12 — Hard cap on grand plots emitted regardless of dial/city size.
2375
2432
  - `LOCK_ACTIONS` (const): const LOCK_ACTIONS: readonly LockAction[] — The five pick actions, in display order (shallow → deep).
2433
+ - `LaneEdgeConfig` (interface): interface LaneEdgeConfig — One directed lane span to resolve into the graph.
2434
+ - `LaneGraph` (interface): interface LaneGraph — Authored lane topology: junctions, directed spans with speed limits and arc lengths, crossings with their right-of-way, and parking spots. Positions come from the spans themselves, so a traffic runtime never re-derives geometry or runs a path search per vehicle.
2435
+ - `LaneGraphConfig` (interface): interface LaneGraphConfig — The authored lane graph: junctions, directed spans, crossings and parking spots.
2436
+ - `LaneGraphDocumentOptions` (interface): interface LaneGraphDocumentOptions — How the document reader recognizes authored lane content.
2437
+ - `LaneIntersection` (interface): interface LaneIntersection — A resolved crossing: the lanes that feed it, the lanes that leave it, and its right-of-way.
2438
+ - `LaneIntersectionConfig` (interface): interface LaneIntersectionConfig — Right-of-way and signal timing for one authored crossing.
2439
+ - `LaneNode` (interface): interface LaneNode — A junction where lane spans meet.
2440
+ - `LanePose` (interface): interface LanePose — World pose of a lane at an arc position.
2441
+ - `LaneRightOfWay` (type): type LaneRightOfWay = "signal" | "priority" — How an intersection resolves which approach may enter next.
2442
+ - `LaneSignalProgram` (interface): interface LaneSignalProgram — Signal timing for one intersection; phases list the in-lanes each green window serves.
2443
+ - `LaneSpan` (interface): interface LaneSpan — A resolved lane span: centerline, arc lengths, limit and adjacency.
2444
+ - `LaneSpot` (interface): interface LaneSpot — A parking spot projected onto one lane at an arc position.
2376
2445
  - `LeaveVehicleOptions` (interface): interface LeaveVehicleOptions extends DismountOffset — Exit placement and optional local camera restoration for an explicit rider.
2377
2446
  - `LineFormationOptions` (interface): interface LineFormationOptions — Options for {@link lineFormation}.
2378
2447
  - `LocalAvoidanceOptions` (interface): interface LocalAvoidanceOptions — Tuning for {@link resolveLocalAvoidance}.
@@ -2667,6 +2736,14 @@
2667
2736
  - `TimerSnapshot` (interface): interface TimerSnapshot — One timer's serializable state — elapsed resolved at snapshot time, ready to re-anchor on restore.
2668
2737
  - `TimerStartOptions` (interface): interface TimerStartOptions — Options for {@link TimerSet.start}.
2669
2738
  - `ToneVoice` (interface): interface ToneVoice — A pitched oscillator voice: a 12ms linear attack to `gain`, then an exponential decay to silence across `duration`, with an optional exponential pitch slide from `freq` to `slideTo`.
2739
+ - `Traffic` (interface): interface Traffic — Traffic on an authored lane graph: cars follow lane spans with IDM car-following, hold at stop lines, cross intersections under a signal phase or first-come right-of-way, change lanes, park in authored spots, and grow or shrink with the caller's `demand`. Deterministic — cars advance in insertion order, ties break on id, and the injected generator is the only source of variety. Bounded per tick: one pass over the cars, one sorted pass per occupied lane, no path search.
2740
+ - `TrafficCarView` (interface): interface TrafficCarView — One car's live state, as a renderer, a game or the devtools overlay reads it.
2741
+ - `TrafficOptions` (interface): interface TrafficOptions — Everything {@link createTraffic} needs: the graph, the injected generator and the caller's policies.
2742
+ - `TrafficRoutePolicy` (type): type TrafficRoutePolicy = (context: { carId: string; laneId: string; nodeId: string; /** Outbound lanes that are not the lane the car arrived on, sorted by id. */ options: readonly string[]; }) => string | null — Caller-supplied exit choice at a junction: return one of `options`, or `null` to leave the graph.
2743
+ - `TrafficSignalState` (interface): interface TrafficSignalState — An intersection's signal right now: which phase is green and for how long.
2744
+ - `TrafficSnapshot` (interface): interface TrafficSnapshot — Serializable traffic state: the fleet, the parking occupancy, the signal clocks and the demand.
2745
+ - `TrafficStats` (interface): interface TrafficStats — Counters a devtools probe, a HUD or a test reads.
2746
+ - `TrafficTuning` (interface): interface TrafficTuning — Every per-car behaviour number, retunable in play.
2670
2747
  - `TravelPointDef` (interface): interface TravelPointDef<TMeta = unknown> — A fast-travel destination the game defines. Discovery is tracked separately.
2671
2748
  - `TravelPointView` (interface): interface TravelPointView<TMeta = unknown> extends TravelPointDef<TMeta> — A destination plus its discovery state (and distance from a query origin, when given).
2672
2749
  - `TriggerActionDefinition` (interface): interface TriggerActionDefinition — A game-declared action the editor can assign to a volume/marker trigger. Schema drives the inspector params; `targets`/`events` optionally narrow where it appears.
@@ -2693,9 +2770,23 @@
2693
2770
  - `VISIBILITY_OBSERVED` (const): const VISIBILITY_OBSERVED: 2 — Numeric code for a cell a group currently observes.
2694
2771
  - `VISIBILITY_REMEMBERED` (const): const VISIBILITY_REMEMBERED: 1 — Numeric code for a cell seen before but not currently in view (last-known).
2695
2772
  - `Vec3` (type): type Vec3 = EntityPosition — ⚠ undocumented · used by `catenaryCurve`: True hyperbolic catenary between two anchors — the shape a uniform cable actually takes under gravity.
2773
+ - `VehicleDamage` (interface): interface VehicleDamage — Fixed-zone crash damage for a vehicle: impacts accumulate into discrete stages, stages change handling and show a bounded dent, and a part can come off at a set stage. Data-driven and bounded by construction — the zone set is authored, each zone's deformation is one capped displacement, and no mesh is edited. Serializes through {@link snapshot}/{@link restore}, so a save, a replay or a host correction resumes mid-crash.
2774
+ - `VehicleDamageConfig` (interface): interface VehicleDamageConfig — Everything {@link createVehicleDamage} needs.
2775
+ - `VehicleDamageEffect` (interface): interface VehicleDamageEffect — Handling change one damage zone applies at its *last* stage. Multiplier fields read `1` = undamaged, and a zone part-way through its stages gets the fraction between; `steerPull` and `drag` are additive and reach their full value only at the last stage.
2776
+ - `VehicleDamageEffectResolved` (interface): interface VehicleDamageEffectResolved — Every zone's handling cost summed into one set of numbers (all fields present, defaults filled).
2777
+ - `VehicleDamageState` (interface): interface VehicleDamageState — Serializable damage state: {@link VehicleDamage.restore} needs only this.
2778
+ - `VehicleDamageTransition` (interface): interface VehicleDamageTransition — A zone crossing a threshold this tick — the event a HUD feed, a smoke puff or a camera shake keys off.
2779
+ - `VehicleDamageZone` (interface): interface VehicleDamageZone — One damageable zone: a fixed local body point, the impulse thresholds that step it through its stages, the bounded dent it shows, and the handling it costs. There is no mesh editing — a zone's deformation is one displacement it can never exceed, so the visual can never grow without limit.
2780
+ - `VehicleDamageZoneState` (interface): interface VehicleDamageZoneState — Live state of one zone, as a HUD or a save reads it.
2781
+ - `VehicleDeformation` (interface): interface VehicleDeformation — A zone's current dent: a bounded local displacement and whether the part has come off.
2782
+ - `VehicleFeel` (interface): interface VehicleFeel — Comparable handling numbers derived from a tuning — what "the numbers and the feel" means to a shop screen, a HUD or a test. Grip is reported as the curve's own shape, so a compound change shows up as a moved peak rather than a single scalar.
2696
2783
  - `VehicleImpact` (interface): interface VehicleImpact — A single blocked-move event a car can react to (#1051): crash damage, screen shake, a metal-crunch cue. Reported once, when a move is first vetoed, then consumed by {@link VehicleObstacleClamp.takeImpact}.
2697
2784
  - `VehicleObstacleClamp` (interface): interface VehicleObstacleClamp — A planar move clamp for a kinematic car: feed {@link clampMove} to a vehicle's `clampMove` hook so an attempted XZ displacement slides along world solids instead of driving through them, and read {@link takeImpact} once per tick for the crash it produced.
2785
+ - `VehiclePart` (interface): interface VehiclePart — One installable part. `slot` is the caller's own grouping ("tires", "engine", "front-bumper") — the engine does not name slots, so a game's upgrade tree stays its own. Effects compose in install order, and a part's `gripCurve` replaces the base tire compound outright (last one installed wins).
2786
+ - `VehiclePartEffect` (interface): interface VehiclePartEffect — How one part moves a stat: multiply the base by `scale`, then add `add`. Both default to no change.
2698
2787
  - `VehicleSeats` (class): class VehicleSeats — Composes `scene/mount`'s control-transfer bookkeeping with the seat/camera/movement-mode transition every enter/exit-vehicle flow needs (#533.2): boarding resolves a free seat and reports the camera target, drive target, and rider movement-lock patch in one call; leaving computes a side-door placement next to the vehicle and reports the same triad in reverse. Pure — no entity/camera side effects — the caller applies `riderMovementPatch`/`placement`/`cameraTarget` via its own `ctx`.
2788
+ - `VehicleSetup` (interface): interface VehicleSetup — A composed vehicle: the tuning to hand the sim, the feel it produces, and what was installed.
2789
+ - `VehicleStat` (type): type VehicleStat = | "engineAccel" | "brakeAccel" | "topSpeed" | "reverseSpeed" | "turnRate" | "turnSpeedRef" | "gripStrength" | "handbrakeGrip" | "rollingResistance" | "coastDeceleration" — A tunable vehicle number a part can move; each names a {@link KinematicVehicleTuning} field.
2699
2790
  - `VisibilityCells` (interface): interface VisibilityCells — Dense per-cell readout for one group, shaped for minimap/overlay drawing.
2700
2791
  - `VisibilityConfig` (interface): interface VisibilityConfig — Per-game visibility configuration, surfaced on `PlayableGame.visibility`. Everything is optional: an existing game that sets nothing gets the conservative engine defaults automatically. This is the scene-level and per-kind override seam (requirement: per-object, per-layer, per-scene, and global controls).
2701
2792
  - `VisibilityDelta` (interface): interface VisibilityDelta — The cells that changed state for one group in a single {@link VisibilityField.observe} call — a pure incremental readout for rendering, minimaps, AI, UI, and authoritative replication. Never lists unchanged cells, so it stays bounded by how much the group's view actually moved.
@@ -2751,6 +2842,7 @@
2751
2842
  - `applyPathProfiles` (function): function applyPathProfiles(field: TerrainField, profiles: readonly TerrainPathProfile[]): TerrainField — Composes authored path profiles onto a field — the shared seam that turns a scene road/river/ramp path into flattened, graded, carved, or retained ground. Applies after `flatten` masks in `resolveTerrainField` so a corridor grades over already-leveled pads; call it directly to layer profiles onto any field a game builds by hand. Returns the field unchanged when no profile is given.
2752
2843
  - `applyRotationPolicy` (function): function applyRotationPolicy(degrees: number, policy: PlacementRotationPolicy | undefined): number — Resolve a raw drag/scroll heading through a {@link PlacementRotationPolicy} into the heading a placement should actually commit: `locked` ignores the input for its fixed heading, `snap` quantizes, `free` (or an absent policy) normalizes. Deterministic — the single interpreter preview, editor placement, and runtime spawning share instead of each re-implementing the modes.
2753
2844
  - `applySurfaceDeltaToSnapshot` (function): function applySurfaceDeltaToSnapshot(snapshot: TerraformSnapshot, delta: SurfaceDelta): TerraformSnapshot — Returns a new snapshot with a surface delta's `after` ids applied (copy-on-write).
2845
+ - `applyVehicleParts` (function): function applyVehicleParts(base: KinematicVehicleTuning, parts: readonly VehiclePart[]): VehicleSetup — Fold parts onto a base tuning: every effect scales then adds onto the running value, in array order, so a setup is deterministic and a caller can reason about which part moved what. The base tuning is never mutated, so the same base can seed several builds.
2754
2846
  - `assignFormationSlots` (function): function assignFormationSlots(members: readonly Vec2[], slots: readonly Vec2[], options: SlotAssignmentOptions = {}): number[] — Match `members` to `slots` by a deterministic greedy nearest assignment: every (member, slot) pair is ranked by squared travel distance (minus a stickiness bonus for a member's previous slot) and assigned in order while both ends are free. Returns `assignment[member] = slot`, or `-1` for members left unmatched when there are fewer slots than members. Ties break by member then slot index, so the result is stable across runs and independent of input ordering — the property replay and lockstep multiplayer rely on. Groups are bounded, so the `O(members·slots·log)` sort is fine; this is not a per-frame whole-world pass.
2755
2847
  - `bakeMinimapBackground` (function): function bakeMinimapBackground(source: MinimapBakeSource, options: MinimapBakeOptions = {}): { background: string; mapBounds: MinimapBakeBounds } — One-call convenience: rasterize a source and return `{ background, mapBounds }` for a minimap.
2756
2848
  - `bakeMinimapImage` (function): function bakeMinimapImage(source: MinimapBakeSource, options: MinimapBakeOptions = {}): MinimapBake — Rasterizes a top-down terrain image: height maps onto a low→high color ramp (normalized over the sampled height range), slope darkens the shade, water fills below `waterLevel`, and each zone polygon tints the ground it covers. Deterministic for a fixed source. The bounds are preserved so a player-centered minimap pans the image correctly.
@@ -2819,6 +2911,7 @@
2819
2911
  - `createInterestCensus` (function): function createInterestCensus(): InterestCensusAccumulator — Create a census accumulator so the caller can tally active/dormant gates during the loop it already runs, avoiding any separate full-world scan just to report scheduler metrics.
2820
2912
  - `createInterestGateState` (function): function createInterestGateState(config: InterestSchedulerConfig, phase = 0): InterestGateState — Create a dormant gate state, staggered by `phase` (`[0,1)`, typically {@link interestPhase} of the agent id) so siblings created together do not all fire their first active tick on the same frame.
2821
2913
  - `createKinematicVehicle` (function): function createKinematicVehicle(initialTuning: KinematicVehicleTuning, options: KinematicVehicleOptions = {}): KinematicVehicle — Arcade ground-vehicle sim (car, kart, bike) with throttle, brake, steer, drift and retunable handling.
2914
+ - `createLaneGraph` (function): function createLaneGraph(config: LaneGraphConfig): LaneGraph — Read handle over the graph resolved once at construction. The build mutates its working set; this object never changes, so a save system rebuilds it from the scene document instead of serializing it.
2822
2915
  - `createLeaderTrail` (function): function createLeaderTrail(config: LeaderTrailConfig): LeaderTrail — A trailing follower formation that chases a leader along its past path — snake/convoy trails.
2823
2916
  - `createLodScheduler` (function): function createLodScheduler(config: LodSchedulerConfig): LodScheduler — ⚠ undocumented
2824
2917
  - `createMarkerSet` (function): function createMarkerSet<TMeta = unknown>(now: () => number = Date.now): MarkerSet<TMeta> — ⚠ undocumented
@@ -2854,8 +2947,10 @@
2854
2947
  - `createTerrainSnapshot` (function): function createTerrainSnapshot(config: EditableTerrainConfig): TerraformSnapshot — A fresh, unedited terrain snapshot sized to `bounds`/`cellSize` — the seed for a new sculpt document.
2855
2948
  - `createThreatTable` (function): function createThreatTable(config: ThreatTableConfig = {}): ThreatTable — ⚠ undocumented
2856
2949
  - `createTimerSet` (function): function createTimerSet(options: TimerSetOptions = {}): TimerSet — Create a serializable set of named countdown / countup timers on an injected clock — one primitive for round timers, respawn clocks, and ability cooldown/charge. Start/pause/resume/stop/reset timers by free-string id, read `{ remainingMs, elapsedMs, durationMs, progress01, running, expired }` for a mm:ss readout or a radial/bar fill, and `poll` for expiry edges. Ids and labels carry no genre meaning; `snapshot`/`restore` round-trip through a save.
2950
+ - `createTraffic` (function): function createTraffic(options: TrafficOptions): Traffic — Traffic for an authored lane graph (see {@link createLaneGraph}).
2857
2951
  - `createUtilityBrain` (function): function createUtilityBrain(config: UtilityBrainConfig): UtilityBrain — Create a utility brain over an existing {@link NeedSet}. The need set is injected, not owned, so the game shows the same hunger the brain scores and persists one copy.
2858
2952
  - `createVehicleBody` (function): function createVehicleBody(world: PhysicsWorld, config: VehicleBodyConfig): VehicleBody — ⚠ undocumented
2953
+ - `createVehicleDamage` (function): function createVehicleDamage(config: VehicleDamageConfig): VehicleDamage — Fixed-zone crash damage (issue #86's stage model) wired to handling and visuals: see {@link VehicleDamage}.
2859
2954
  - `createVehicleObstacleClamp` (function): function createVehicleObstacleClamp(options: { /** Solids near the car this tick (already filtered to the relevant, solid set). */ obstacles: () => readonly CollisionObstacle[]; /** Vehicle body radius (units). Default {@link DEFAULT_VEHICLE_RADIUS}. */ radius?: number; /** Current tick dt (seconds)… — Build a slide-along move clamp for a kinematic car (#1051). `obstacles` is sampled fresh each tick — the caller hands back the already-filtered set of solids near the car — and `dt` supplies the current tick length so a blocked move's lost displacement converts to a closing speed. `radius` inflates each obstacle footprint by the car's body radius (default {@link DEFAULT_VEHICLE_RADIUS}).
2860
2955
  - `createVehicleSeats` (function): function createVehicleSeats(controller?: MountController): VehicleSeats — Builds a {@link VehicleSeats}, optionally over an existing `MountController` to share its occupancy.
2861
2956
  - `createVisibilityField` (function): function createVisibilityField(config: VisibilityFieldConfig): VisibilityField — Build a per-group gameplay visibility field. First adopters: an RTS/stealth team whose scout observes terrain (which stays remembered after it leaves) while enemy units re-hide once no scout observes their cell, and any authoritative host that must filter hidden entities out of a client's replica.
@@ -2864,6 +2959,8 @@
2864
2959
  - `createWaveRunner` (function): function createWaveRunner(config: WaveRunnerConfig): WaveRunner — A thin, stateful, observable wrapper around the seeded {@link createSpawnDirectorState} / {@link advanceSpawnDirector} model. It OWNS a {@link SpawnDirectorState}, ticks it from `update(dt, ctx?)`, keeps the new state, and forwards each emitted {@link SpawnRequest} to an optional `onSpawn` sink so the model never instantiates entities. `view()` returns a pooled per-frame readout — the "brain behind WAVE 3": the 1-based wave number, wave progress `0..1`, banked budget, per-wave and total spawn counts, alert, and done flag. `forceNextWave` / `raiseAlert` drive it, `subscribe` observes it, and `snapshot`/`restore` round-trip the plain serializable state object through a save. Nothing here is genre-specific: spawn-entry `kind`s (the director's `entryId`s) stay free strings the runner never interprets.
2865
2960
  - `createWaypointStore` (function): function createWaypointStore(deps: WaypointStoreDeps = {}): WaypointStore — Player-owned waypoint layer: place/track/clear pins, mirror them into a shared `MarkerSet` (so `Minimap`/`WorldMap`/`Compass` render them for free), and read bearing/distance guidance for an on-screen arrow to the tracked one. State is plain data — `snapshot()`/`restore()` round-trip through any save blob.
2866
2961
  - `customRegion` (function): function customRegion<P extends SamplePoint>(spec: { dimensions: 2 | 3; sample: (rng: () => number) => P; contains: (point: P) => boolean; isEmpty?: boolean; }): SampleRegion<P> — Wrap a caller-defined sampler and bounds test as a {@link SampleRegion} — the escape hatch for regions the built-ins do not cover (a navmesh cell, a heightfield mask, a spline tube).
2962
+ - `damagedAxis` (function): function damagedAxis(axis: AxisInput, effect: VehicleDamageEffectResolved): AxisInput — Apply a damage `steerPull` to a steer axis, clamped to the input range — a bent frame pulling one way.
2963
+ - `damagedTuning` (function): function damagedTuning(base: KinematicVehicleTuning, effect: VehicleDamageEffectResolved): KinematicVehicleTuning — Bake a damage effect into a tuning — the lasting-damage path. Hand the result to the sim's `retune`, which keeps the vehicle's momentum; the base tuning is not mutated, so a repair is the same call with the undamaged base.
2867
2964
  - `dashSegments` (function): function dashSegments(path: readonly RoadPoint[], dashLength = 3, gapLength = 3, exclude: readonly DashExclusion[] = []): readonly (readonly RoadPoint[])[] — Split a centerline into dash sub-polylines for lane markings: `dashLength` of painted line, `gapLength` of asphalt, repeated along the path's arc length. Feed each returned sub-path back through {@link buildRoadRibbon} with a thin width to mesh the dashes. Pass `exclude` circles (junction patches) to interrupt the center line through intersections — any dash whose midpoint lands inside an exclusion is dropped.
2868
2965
  - `defineAttackMoveOrder` (function): function defineAttackMoveOrder<TCtx extends OrderMover & OrderTargeting>(config: EngagementKindConfig = {}): OrderKind<TCtx, AttackMoveOrderPayload> — Attack-move: advance toward a destination but break off to engage any hostile acquired within `aggroRadius`, pursuing it to `attackRange` and writing the engagement intent into `order.state` for the game to act on. Completes on reaching the destination with nothing to engage.
2869
2966
  - `defineHoldOrder` (function): function defineHoldOrder<TCtx extends OrderMover>(config: OrderKindConfig = {}): OrderKind<TCtx, EmptyOrderPayload> — Hold position: stand ground indefinitely (never completes on its own) until preempted or canceled — the "guard here" verb.
@@ -2890,6 +2987,7 @@
2890
2987
  - `extractContours` (function): function extractContours(sampleHeight: HeightSampler, options: ContourOptions): ContourLine[] — Traces surface-following contour lines from a height field via marching squares: samples a bounded grid over the region, then for every multiple of `interval` strictly inside the sampled height range emits the iso-line as XZ segments (saddle cells disambiguated by the cell-center average). Because a contour is exact constant elevation, drawing each segment at `y = level` makes it hug the terrain — the readable, scale-bearing overlay a flat ground grid cannot give. Pure math, renderer-agnostic.
2891
2988
  - `extremumStack` (function): function extremumStack<P>(magnitudeOf: MagnitudeOf<P>, weakest = false): AreaStackPolicy<P> — Reduce to the single strongest membership overall (`weakest = false` picks the weakest); ties broken by `sourceId`. Use when only the best or worst overlapping source should apply.
2892
2989
  - `facingYaw` (function): function facingYaw(from: Vec2, to: Vec2): number — Engine yaw (`rotationY`, radians) that faces from `from` toward `to` on the XZ plane, matching the `forward = (sin yaw, cos yaw)` convention. Feed the result as the `facing` of {@link placeFormation} to orient a formation along its travel direction. Returns `0` when the points coincide.
2990
+ - `feelOf` (function): function feelOf(tuning: KinematicVehicleTuning): VehicleFeel — Read the handling numbers out of any tuning — base or parts-composed.
2893
2991
  - `findPath` (function): function findPath(grid: NavGrid, from: NavPoint, to: NavPoint, options: FindPathOptions = {}): NavPoint[] | null — A* over the walkable grid. Returns a polyline of world-space `[x, z]` waypoints from `from` to `to`, or `null` when no route exists. Blocked start/goal snap to the nearest walkable cell so a click on an obstacle still routes to its edge.
2894
2992
  - `findSchemaPreset` (function): function findSchemaPreset(schema: ParamSchema, id: string): ParamPreset | undefined — Look up a schema preset by id, or undefined — the seam `apply_preset` and games resolve bundles through.
2895
2993
  - `firstImpact` (function): function firstImpact(hits: readonly SceneRaycastHit[]): SceneRaycastHit | null — First impact: nearest hit that blocks, or nearest hit if none block.
@@ -2917,6 +3015,7 @@
2917
3015
  - `keybind` (function): function keybind(actionId: string, label?: string): KeybindPromptDisplay — ⚠ undocumented
2918
3016
  - `label` (function): function label(text: string): LabelPromptDisplay — ⚠ undocumented
2919
3017
  - `laneCenters` (function): function laneCenters(road: RoadEnvironmentDescriptor): readonly [StreetLane, StreetLane] — Two right-hand-traffic lane centerlines for a road — each offset a quarter of the drivable width from the center and ordered in its direction of travel. Feed a lane's `path` straight into `nav/pathFollow` for traffic AI, or use its endpoints as directed car spawn points.
3018
+ - `laneGraphFromDocument` (function): function laneGraphFromDocument(doc: Pick<EditorDocument, "paths" | "markers">, options: LaneGraphDocumentOptions = {}): LaneGraph — Read a lane graph out of an authored scene document. `traffic_lane` paths are one directed lane each; `road` paths (the editor's street bake) become a lane per direction, offset onto their own side of the road; `traffic_intersection` markers carry a signal program and right-of-way in `meta`; `traffic_parking` markers place parking spots. Runtime positions come from the paths, so editing a road moves its traffic instead of leaving a duplicate lane behind.
2920
3019
  - `leaveVehicle` (function): function leaveVehicle(ctx: GameContext, seats: VehicleSeats, riderId: string, options: LeaveVehicleOptions = {}): ExitVehicleResult | { ok: false; reason: "unknown_rider" | "unknown_vehicle" } — Apply dismount placement, restore rider movement/visibility and the configured camera rig.
2921
3020
  - `lineFormation` (function): function lineFormation(options: LineFormationOptions): FormationSlotGenerator — A single rank abreast, centered on the destination and facing forward — a skirmish line or a chorus row. Slots run left→right along the group's right axis.
2922
3021
  - `listTriggerActions` (function): function listTriggerActions(target?: TriggerSourceKind): TriggerActionDefinition[] — Every registered action, optionally filtered by target collection.
@@ -3075,6 +3174,7 @@
3075
3174
  - `wedgeFormation` (function): function wedgeFormation(options: WedgeFormationOptions): FormationSlotGenerator — A "V"/arrowhead with the apex at the destination and arms trailing back — a flying-wedge charge or a goose skein. Slot 0 is the tip; later slots alternate right then left, each rank stepping one `spacing` outward and backward.
3076
3175
  - `weightedRegion` (function): function weightedRegion<P extends SamplePoint>(entries: readonly WeightedRegionEntry<P>[]): SampleRegion<P> — A composite that first picks one member by weight, then delegates to its sampler — the "weighted subregions" distribution policy. `contains` is true when any member contains the point.
3077
3176
  - `windField` (function): function windField(config: WindFieldConfig = {}): WindField — ⚠ undocumented
3177
+ - `withPart` (function): function withPart(parts: readonly VehiclePart[], next: VehiclePart): VehiclePart[] — Swap a part into its slot: any part already in `next.slot` is removed and `next` takes its place in the same install position, so the order (and therefore the composed numbers) stays stable across an upgrade.
3078
3178
  - `withPathProfiles` (function): function withPathProfiles(base: (x: number, z: number) => number, profiles: readonly TerrainPathProfile[]): (x: number, z: number) => number — Wraps a height sampler so each authored `TerrainPathProfile` reshapes it. Profiles apply in list order (later profiles compose over earlier ones, so an intersection's last profile wins its core); each reads the running height as its surrounding ground and the original `base` for `sample` centerline heights. The returned sampler is pure and deterministic. Degenerate profiles (fewer than two points, non-positive width or length) are skipped; with no usable profile the original `base` is returned unchanged.
3079
3179
  - `world` (function): function world(config: PlaceConfig): PlaceWorldFeature — Declares the place a game is played in: substrate (`ground`) + laws (`physics`, `ground.surface`). The thin default start — `flat` with `Infinity` axes and default physics, `board` for 2D — is a complete world; dressing it (sky, foliage, props, sculpt) is editor-authored scene content. Multiple worlds per game are first-class: declare one `world()` per place, each with its own id, ground, and physics. Games that are not a spatial place (pure UI/rules) omit `world` entirely.
3080
3180
  - `worldSockets` (function): function worldSockets(def: ConnectorPieceDef, piece: PlacedPiece): WorldSocket[] — ⚠ undocumented
@@ -3438,6 +3538,16 @@
3438
3538
  - `socketsCompatible` (function): function socketsCompatible(a: ConnectorSocket, b: ConnectorSocket): boolean — ⚠ undocumented
3439
3539
  - `worldSockets` (function): function worldSockets(def: ConnectorPieceDef, piece: PlacedPiece): WorldSocket[] — ⚠ undocumented
3440
3540
 
3541
+ ## @jgengine/core/world/decalKind
3542
+
3543
+ - `DECAL_DEFAULTS` (const): const DECAL_DEFAULTS: DecalRules — Default decal rules: a small dirt smudge lying on the ground.
3544
+ - `DECAL_KIND` (const): const DECAL_KIND: "decal" — The editor marker kind marking a surface decal.
3545
+ - `DECAL_SCHEMA` (const): const DECAL_SCHEMA: ParamSchema — The decal parameter schema — drives the inspector and the `meta` parse.
3546
+ - `DECAL_STYLES` (const): const DECAL_STYLES: readonly ["dirt", "cracks", "paint", "blood"] — The four authored decal looks; each has its own deterministic procedural texture.
3547
+ - `DecalRules` (interface): interface DecalRules — Fully-defaulted decal params parsed from a marker's `meta`.
3548
+ - `DecalStyle` (type): type DecalStyle = (typeof DECAL_STYLES)[number] — One of the four authored decal looks.
3549
+ - `ResolvedDecal` (interface): interface ResolvedDecal — A resolved decal: its placed point/facing plus fully-defaulted rules.
3550
+
3441
3551
  ## @jgengine/core/world/envField
3442
3552
 
3443
3553
  - `EnvironmentField` (interface): interface EnvironmentField { sample(x: number, z: number, time: number, y?: number): EnvironmentSample; temperature(x: number, z: number, time: number, y?: number): number; wetness(x: number, z: number, time: number): number; lightExposure(x: number, z: number, time:… — ⚠ undocumented · used by `createEnvironmentField`: A sampleable environment field: read temperature, wetness, sun/sky exposure, and ambient light at any world position and time.
@@ -3494,7 +3604,7 @@
3494
3604
  - `EnvironmentWorldConfig` (interface): interface EnvironmentWorldConfig { terrain?: TerrainEnvironmentDescriptor; islands?: readonly TerrainIslandDescriptor[]; sky?: SkyEnvironmentDescriptor; weather?: EnvironmentDescriptorList<WeatherEnvironmentDescriptor>; vegetation?: EnvironmentDescriptorList<VegetationEnvi… — ⚠ undocumented · used by `environment`: Composes an `environment()` feature from terrain, sky, weather, vegetation, water, structures, roads, and pads.
3495
3605
  - `EnvironmentWorldFeature` (interface): interface EnvironmentWorldFeature { kind: "environment"; terrain?: TerrainEnvironmentDescriptor; islands?: readonly TerrainIslandDescriptor[]; sky?: SkyEnvironmentDescriptor; weather?: readonly WeatherEnvironmentDescriptor[]; vegetation?: readonly VegetationEnvironmentDescr… — ⚠ undocumented · used by `environment`: Composes an `environment()` feature from terrain, sky, weather, vegetation, water, structures, roads, and pads.
3496
3606
  - `GrassEnvironmentConfig` (interface): interface GrassEnvironmentConfig { area?: EnvironmentArea; position?: EnvironmentVec2; density?: number; bladeHeight?: readonly [number, number]; bladeWidth?: number; windStrength?: number; colors?: readonly string[]; seed?: string; bladeBend?: readonly [number, number]; t… — ⚠ undocumented · used by `grass`: Declares a grass vegetation patch for `environment()` — area, blade sizing, density, and colors.
3497
- - `GrassEnvironmentDescriptor` (type): type GrassEnvironmentDescriptor = { kind: "grass" } & Required< Pick<GrassEnvironmentConfig, "area" | "density" | "bladeHeight" | "bladeWidth" | "windStrength" | "colors"> > & Pick<GrassEnvironmentConfig, "seed" | "bladeBend" | "tuftRadius" | "colorVariation" | "normalLift"> — ⚠ undocumented · used by `grass`: Declares a grass vegetation patch for `environment()` — area, blade sizing, density, and colors.
3607
+ - `GrassEnvironmentDescriptor` (type): type GrassEnvironmentDescriptor = { kind: "grass" } & Required< Pick<GrassEnvironmentConfig, "area" | "density" | "bladeHeight" | "bladeWidth" | "windStrength" | "colors"> > & Pick<GrassEnvironmentConfig, "seed" | "bladeBend" | "tuftRadius" | "colorVariation" | "normalLift" | "groundCover"> — ⚠ undocumented · used by `grass`: Declares a grass vegetation patch for `environment()` — area, blade sizing, density, and colors.
3498
3608
  - `OceanEnvironmentConfig` (interface): interface OceanEnvironmentConfig { bounds?: WorldBounds; position?: EnvironmentVec2; level?: number; levelAt?: (time: number) => number; waveHeight?: number; waveScale?: number; waveSpeed?: number; color?: string } — ⚠ undocumented · used by `ocean`: Declares an ocean water body for `environment()` — bounds, level, and wave tuning.
3499
3609
  - `OceanEnvironmentDescriptor` (type): type OceanEnvironmentDescriptor = { kind: "ocean" } & Required< Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color"> > & Pick<OceanEnvironmentConfig, "position" | "levelAt"> — ⚠ undocumented · used by `ocean`: Declares an ocean water body for `environment()` — bounds, level, and wave tuning.
3500
3610
  - `PadEnvironmentConfig` (interface): interface PadEnvironmentConfig { center: EnvironmentVec2; size: PadSize; height?: number; elevation?: number; color?: string; rotationY?: number } — ⚠ undocumented
@@ -462,6 +462,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
462
462
 
463
463
  - `createKinematicVehicle` (function) · `import { createKinematicVehicle } from "@jgengine/core/world"`
464
464
 
465
+ ## lane-graph — directed lane topology with junctions, signals and parking for vehicle traffic
466
+
467
+ - `createLaneGraph` (function) · `import { createLaneGraph } from "@jgengine/core/world"`
468
+
469
+ ## lane-graph-from-scene — read an authored lane graph, signals and parking out of the scene document
470
+
471
+ - `laneGraphFromDocument` (function) · `import { laneGraphFromDocument } from "@jgengine/core/world"`
472
+
465
473
  ## lean-metrics — measure a motorcycle's lean — steady lean, time to lean, countersteer tip-in
466
474
 
467
475
  - `measureLean` (function) · `import { measureLean } from "@jgengine/core/physics/handlingProbe"`
@@ -897,6 +905,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
897
905
  - `setTouchControlsMode` (function) · `import { setTouchControlsMode } from "@jgengine/core/input/touchControlsMode"`
898
906
  - `touchButtonShape` (function) · `import { touchButtonShape } from "@jgengine/core/input/touchScheme"`
899
907
 
908
+ ## traffic — lane-graph traffic with car following, signals, right-of-way, lane changes and parking
909
+
910
+ - `createTraffic` (function) · `import { createTraffic } from "@jgengine/core/world"`
911
+
900
912
  ## ui-sound-cues — default procedural click/hover/open/close/confirm/deny/notify cues on a "ui" bus, overridable per game
901
913
 
902
914
  - `withUiCueSounds` (function) · `import { withUiCueSounds } from "@jgengine/core/audio/uiCues"`
@@ -913,10 +925,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
913
925
 
914
926
  - `createVehicleBackendLink` (function) · `import { createVehicleBackendLink } from "@jgengine/core/physics/vehicleBackendLink"`
915
927
 
928
+ ## vehicle-damage — fixed-zone crash damage that changes handling and shows a bounded dent, serializable
929
+
930
+ - `createVehicleDamage` (function) · `import { createVehicleDamage } from "@jgengine/core/world"`
931
+
916
932
  ## vehicle-dynamics — force-based car handling — tires, load transfer, drivetrain, assists — tuned with physical numbers
917
933
 
918
934
  - `createVehicleDynamics` (function) · `import { createVehicleDynamics } from "@jgengine/core/physics/vehicleDynamics"`
919
935
 
936
+ ## vehicle-parts — compose data-driven tuning parts (tires, engine, brakes) onto a vehicle tuning and read the resulting feel
937
+
938
+ - `applyVehicleParts` (function) · `import { applyVehicleParts } from "@jgengine/core/world"`
939
+
920
940
  ## vehicle-seats — enter, exit and swap seats in a car or mount (GTA-style boarding) with driver and passenger slots
921
941
 
922
942
  - `createVehicleSeats` (function) · `import { createVehicleSeats } from "@jgengine/core/world"`
@@ -5,6 +5,10 @@
5
5
  ## The seams
6
6
 
7
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
+ - **Arcade sim.** `createKinematicVehicle(tuning, { surfaceFriction?, dragAt?, clampMove? })` from `@jgengine/core/physics/kinematicVehicle` is the cheap steer-yaw model for chases and ambient cars. Its grip is a `GripCurve` (slip → force) sampled each tick against surface friction, handbrake and downforce; `weightTransfer: { comHeight, wheelbase, trackWidth, sensitivity? }` cuts that curve as weight moves under braking/traction/cornering and reports `weightTransfer`/`loadGrip`. `retune()` swaps handling in play, and `gripScale` on the per-tick modifiers is the one-frame hook. Use the force model above when balance has to come out of the numbers.
9
+ - **Parts.** `applyVehicleParts(base, parts)` (`@jgengine/core/physics/vehicleParts`) folds data-driven parts onto a tuning — `{ id, slot, effects: { engineAccel: { scale, add } }, gripCurve? }` — in install order, and returns the composed tuning plus a comparable `VehicleFeel`. `withPart` swaps one part in its slot, `feelOf(tuning)` reads the numbers out of anything, and `vehicle.retune(setup.tuning)` installs it mid-play without losing momentum.
10
+ - **Damage.** `createVehicleDamage({ zones, disableAt?, onTransition? })` (`@jgengine/core/physics/vehicleDamage`) takes local hit points and impulses, steps fixed zones through thresholds, and returns bounded `deformation()` (a per-zone offset the renderer applies, plus `detached`) and `effect()` (grip/drive/brake/top-speed scales, `steerPull`, `drag`). Feed `effect()` through `damagedTuning(base, effect)` + `retune` for lasting damage, or `modifiers()` for one frame; `damagedAxis(axis, effect)` applies the bent-frame pull. It serializes, so a save or a host correction resumes mid-crash. There is no mesh editing: every zone has one authored `deform` limit.
11
+ - The dent is rendered by the game's own `renderEntity` (it already receives the entity each render) — the zone's `offset` displaces that part, and `detached` hides it. No shell seam is needed.
8
12
  - **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
13
  - **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
14
  - **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.
@@ -36,6 +40,7 @@
36
40
  - **Sound.** Play a one-shot on `step.landingSpeed` (thud, suspension clunk).
37
41
  - **Proof.** For jumps and air control, `measureAir` reports apex height and time, double-jump height, and air rotation rates.
38
42
  - **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.
43
+ - **Proof.** For the arcade sim, `feelOf(setup.tuning)` gives comparable numbers per build, and a test drives the same scripted inputs on two `surfaceFriction` values (or before/after `damagedTuning`) and asserts the grip difference — slip and lateral speed retained, and top speed reached.
39
44
 
40
45
  ## Workflow
41
46