@jgengine/core 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 (55) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/game/playableGame.d.ts +19 -2
  3. package/dist/gameplay.d.ts +5 -0
  4. package/dist/gameplay.js +5 -0
  5. package/dist/inventory/carryLoad.d.ts +48 -0
  6. package/dist/inventory/carryLoad.js +44 -0
  7. package/dist/inventory/equipment.d.ts +95 -0
  8. package/dist/inventory/equipment.js +169 -0
  9. package/dist/inventory/inventoryModel.d.ts +15 -6
  10. package/dist/inventory/inventoryModel.js +54 -5
  11. package/dist/inventory/shapedGrid.d.ts +26 -0
  12. package/dist/inventory/shapedGrid.js +47 -0
  13. package/dist/loot/lootGen.d.ts +186 -0
  14. package/dist/loot/lootGen.js +437 -0
  15. package/dist/loot/lootWeapon.d.ts +35 -0
  16. package/dist/loot/lootWeapon.js +70 -0
  17. package/dist/meta/changelog.d.ts +1 -1
  18. package/dist/meta/changelog.js +37 -1
  19. package/dist/nav/laneGraph.d.ts +178 -0
  20. package/dist/nav/laneGraph.js +671 -0
  21. package/dist/nav/traffic.d.ts +223 -0
  22. package/dist/nav/traffic.js +919 -0
  23. package/dist/physics/kinematicVehicle.d.ts +36 -0
  24. package/dist/physics/kinematicVehicle.js +15 -1
  25. package/dist/physics/vehicleDamage.d.ts +136 -0
  26. package/dist/physics/vehicleDamage.js +214 -0
  27. package/dist/physics/vehicleParts.d.ts +62 -0
  28. package/dist/physics/vehicleParts.js +65 -0
  29. package/dist/progression/gearScore.d.ts +56 -0
  30. package/dist/progression/gearScore.js +59 -0
  31. package/dist/progression/skillTree.d.ts +129 -0
  32. package/dist/progression/skillTree.js +265 -0
  33. package/dist/progression/xpCurve.d.ts +60 -0
  34. package/dist/progression/xpCurve.js +141 -0
  35. package/dist/scene/builtinSceneKinds.js +2 -0
  36. package/dist/world/decalKind.d.ts +48 -0
  37. package/dist/world/decalKind.js +86 -0
  38. package/dist/world/features.d.ts +3 -1
  39. package/dist/world/features.js +1 -0
  40. package/dist/world/grassKind.d.ts +5 -0
  41. package/dist/world/grassKind.js +3 -0
  42. package/dist/world/terraform.d.ts +8 -0
  43. package/dist/world.d.ts +5 -1
  44. package/dist/world.js +4 -0
  45. package/package.json +1 -1
  46. package/skills/jgengine/api.md +2 -2
  47. package/skills/jgengine-combat/SKILL.md +3 -1
  48. package/skills/jgengine-gameplay/SKILL.md +31 -0
  49. package/skills/jgengine-gameplay/api.md +150 -6
  50. package/skills/jgengine-gameplay/capabilities.md +48 -3
  51. package/skills/jgengine-gameplay/reference-systems.md +118 -0
  52. package/skills/jgengine-world/SKILL.md +4 -0
  53. package/skills/jgengine-world/api.md +115 -5
  54. package/skills/jgengine-world/capabilities.md +20 -0
  55. package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
@@ -28,6 +28,124 @@ and level state, then emits every reached level in ascending order. State stays
28
28
  in the caller's save format and round-trips as ordinary JSON. See the
29
29
  [portable XP/leveling recipe](recipes/portable-xp-leveling.md).
30
30
 
31
+ ## Procedural gear
32
+
33
+ `loot/lootGen` rolls a whole named item from a caller-authored `LootCatalog`:
34
+ bases (`baseStats`, affix `pools`, mount `slots`), parts, affix pools, weighted
35
+ `RarityTier`s, `LootManufacturerDef`s (name, flat `bias`, per-stat `scale`,
36
+ favored pools) and a `levelScale` curve of `[level, multiplier]` points. A roll
37
+ picks maker, base, rarity, one part per slot and `affixCount` affixes; stats are
38
+ `base × rarity.statScale × levelScale`, plus part stats/multipliers, maker
39
+ personality, then additive affixes and multiplicative ones. Additive affix
40
+ rolls scale with level; multiplicative ones do not. Names follow one rule:
41
+ maker, rarity word, first part word, first prefix affix, base name, first suffix
42
+ affix. Every random draw uses the injected `rng`, so a seed reproduces the item.
43
+
44
+ ```ts
45
+ import { compareLootItems, createLootRoller, lootWeaponDefinition, seededRng } from "@jgengine/core/gameplay";
46
+
47
+ const roller = createLootRoller(catalog, { devtoolsName: "loot" });
48
+ const item = roller.roll(seededRng(dropSeed), { level: 30 });
49
+ const delta = compareLootItems(item, equipped, { lowerIsBetter: ["reload"] });
50
+ const weapon = lootWeaponDefinition(item, { template, stats: { damage: "power", roundsPerMinute: "cadence" } });
51
+ ```
52
+
53
+ `validateLootItem` returns coded issues (`unknown-base`, `part-category`,
54
+ `missing-slot-part`, `unknown-affix`, `empty-name`…) and the roller's counters
55
+ snapshot/restore as plain data. Register a roll through
56
+ `proceduralLootEntry` into `item/itemInstanceRegistry` and hand its runtime id
57
+ to `game/lootTable`; the weapon projection only maps stats it is told to, so a
58
+ catalog without weapon stats stays usable for non-weapon gear. Check a
59
+ distribution with a fixed seed over 10,000 drops against each tier's 4σ
60
+ binomial bound rather than eyeballing shares.
61
+
62
+ ## Progression and gear score
63
+
64
+ `progression/xpCurve` turns one authored growth rule into the level table, the cap and a
65
+ plain-words rule line, so the balance lives in save-visible data:
66
+
67
+ ```ts
68
+ import { createSkillTree, defineXpCurve, gearScore } from "@jgengine/core/gameplay";
69
+
70
+ const xp = defineXpCurve({ growth: "power", step: 100, growthRate: 1.5, maxLevel: 40, round: "floor" });
71
+ // rule: "power: 100 x level^1.5, floor (levels 1-40)"; xpForLevel(2) is 282, xpForLevel(40) is 0
72
+ xp.levelForXp(xp.totalXpFor(7)).level; // 7
73
+ xp.track(); // the same thresholds as a `leveling` track for a stat adapter
74
+
75
+ const tree = createSkillTree({ nodes, synergies, points: 6 }, { devtoolsName: "skills" });
76
+ tree.spend("strike"); // { ok: true, spent: 1, pointsAvailable: 5 }
77
+ tree.canSpend("sight"); // { ok: false, reason: "requires", detail: '...needs rank 1 of "focus"' }
78
+ const { stats, actives, synergies: live } = tree.resolved();
79
+ tree.respec(); // { refunded: 6, pointsAvailable: 6 }
80
+ ```
81
+
82
+ A `SkillNodeDef` is `{ id, branch?, cost?, maxRank?, requires?, requiresPointsInBranch?,
83
+ excludes?, passives?, actives?, tags? }`; a `SkillSynergyDef` lists at least two `requires` and
84
+ adds passives/actives only while they all hold. `excludes` is symmetric, so two nodes named as
85
+ alternatives block each other in both directions. Actives are `{ id, cooldownMs?, resourceCost?,
86
+ castType?, groups? }`, shaped to bind into `createAbilityKit` slots. Snapshot/restore carries
87
+ `{ points, ranks }`; restore refuses unknown nodes, over-ranked nodes and negative points without
88
+ mutating. A tree where breadth beats depth is the point: in the test fixture, four points down
89
+ `strike` give `damage +3`, while the same four points in `strike x2`, `focus`, `sight` give
90
+ `damage +4` plus the `overload` synergy and its action.
91
+
92
+ `gearScore(set, options)` scores `slot -> { stats, level?, rarity? }` as
93
+ `Σ_stat value × weight × (lowerIsBetter ? -1 : 1) + levelWeight × level + rarityScore[rarity]`,
94
+ summed over filled slots, minus `emptySlotPenalty` per expected-but-empty slot; it also returns
95
+ `bySlot`, `byStat`, `average`, `coverage` and `emptySlots`. `compareGearSets` ranks two loadouts.
96
+ The stat term comes from `lootStatContributions`, the same function behind `scoreLootStats` and
97
+ `compareLootItems`, so per-item and whole-loadout scores cannot drift apart.
98
+
99
+ ## Inventory, equipment and weight
100
+
101
+ A carried bag and a stash are two declared inventories, so they hold separate plain state from the
102
+ first frame and persist as `InventorySet.snapshot()`:
103
+
104
+ ```ts
105
+ import { createInventorySet } from "@jgengine/core/inventory/inventoryModel";
106
+ import { canCarry, characterLoad } from "@jgengine/core/inventory/carryLoad";
107
+ import { createEquipment } from "@jgengine/core/inventory/equipment";
108
+
109
+ const items = createInventorySet({ bag: { slots: 24 }, stash: { slots: 60 } }, traits);
110
+ items.move("bag", 3, "stash", 0); // across inventories; a rejected move leaves both untouched
111
+ const saved = items.snapshot(); // detached; JSON-safe; restore() refuses a bad save without mutating
112
+
113
+ const gear = createEquipment(
114
+ [{ id: "weapon", blocks: ["offhand"] }, { id: "offhand" }, { id: "chest", accepts: ["armor"] }],
115
+ (itemId) => baseDefs[registry.get(itemId)?.baseId ?? itemId]?.gear, // rolled item -> its base's gear facts
116
+ { devtoolsName: "gear" },
117
+ );
118
+ gear.equip("chest", "plate"); // { status: "ok", replaced: null, ... }
119
+ gear.equipBest("greatsword"); // first free preferred slot, else the first one it may replace
120
+
121
+ const load = characterLoad({
122
+ carried: [items.state("bag")],
123
+ worn: gear.worn().map((entry) => entry.itemId),
124
+ weightOf: (itemId) => baseDefs[itemId]?.weight ?? 0,
125
+ capacity: 60 + stats.value("carryCapacity"),
126
+ });
127
+ load.moveMultiplier; // 1 unencumbered, decays across the soft band, 0 at capacity
128
+ canCarry(load, 12); // gate a pickup; the overload policy stays the game's
129
+ ```
130
+
131
+ `EquipSlotDef` owns `accepts` (item kinds) and `blocks` (slots it occupies while filled), and
132
+ `EquipItemDef` owns the `slots` an item may occupy plus its `kind`; a filled slot swaps and returns
133
+ the replaced item, so nothing is silently destroyed, and `state()` / `restore()` carry
134
+ `{ slots: [[slotId, itemId]] }`. The stash is simply left out of `carried`, so its contents add no
135
+ weight, and declaring it `tier: "banked"` keeps it out of `partitionOnDeath`'s lost half.
136
+
137
+ For a footprint bag, `fitShapedItem` (`inventory/shapedGrid`) places the first legal spot or names
138
+ why it does not fit — `too-large` with both sizes, `no-space` for a full board, `budget-exceeded` for
139
+ an exhausted search. Stack rules stay in `ItemTraits`: `putItem` fills partial stacks then opens new
140
+ ones up to the max stack, `moveItem` merges with a remainder, and `splitStack(..., limit)` refuses an
141
+ `over-stack` merge instead of breaking the cap.
142
+
143
+ The rendered side is the engine's: `<InventoryGrid inventoryId="bag" />` (`packages/react`) binds a
144
+ live declared inventory with drag, swap, merge and split through the `inventory.move` /
145
+ `inventory.split` commands, and `<EquipmentSlots items={...} />` paints the worn slots from
146
+ `gear.worn()`. What the grid cannot yet do is drag from one inventory to another (bag to stash), so a
147
+ stash transfer goes through `InventorySet.move` behind a button or command.
148
+
31
149
  ## Coherent authored content
32
150
 
33
151
  Keep stable ids and canonical facts for characters, factions, places, and world rules in the game's authored data. Record each chapter's prerequisites, consequences, and which facts it may introduce or change. The game's creative pillars, story voice, content palette, and desired player experience come from its author; generated prose, items, art, and assets must serve those constraints. Reuse constrained generation and validation code across games while each game owns its content and visual identity; do not fill catalogs from default genre kits or mass generic content.
@@ -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