@jgengine/core 0.17.0 → 0.18.1

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 (174) hide show
  1. package/CHANGELOG.md +130 -2
  2. package/dist/ai/decisionGraph.d.ts +52 -0
  3. package/dist/ai/decisionGraph.js +82 -0
  4. package/dist/ai/mobBrainGraph.d.ts +3 -0
  5. package/dist/ai/mobBrainGraph.js +25 -0
  6. package/dist/ai/tacticalQueries.d.ts +30 -0
  7. package/dist/ai/tacticalQueries.js +45 -0
  8. package/dist/anim/animGraph.d.ts +144 -0
  9. package/dist/anim/animGraph.js +286 -0
  10. package/dist/anim/ikSolver.d.ts +51 -0
  11. package/dist/anim/ikSolver.js +124 -0
  12. package/dist/anim/locomotionGraph.d.ts +26 -0
  13. package/dist/anim/locomotionGraph.js +33 -0
  14. package/dist/assets/spriteAtlas.d.ts +21 -0
  15. package/dist/assets/spriteAtlas.js +103 -0
  16. package/dist/audio/audioFalloff.d.ts +10 -0
  17. package/dist/economy/currency.d.ts +16 -2
  18. package/dist/economy/currency.js +55 -14
  19. package/dist/economy/wallet.d.ts +5 -4
  20. package/dist/economy/wallet.js +8 -4
  21. package/dist/editor/commandApply.js +1 -0
  22. package/dist/editor/commands.d.ts +4 -1
  23. package/dist/editor/document.d.ts +2 -1
  24. package/dist/editor/document.js +89 -0
  25. package/dist/editor/environment.d.ts +3 -0
  26. package/dist/editor/environment.js +16 -0
  27. package/dist/editor/index.d.ts +4 -2
  28. package/dist/editor/index.js +1 -1
  29. package/dist/editor/types.d.ts +16 -0
  30. package/dist/game/cameraConfig.d.ts +8 -0
  31. package/dist/game/chat.d.ts +22 -1
  32. package/dist/game/chat.js +48 -19
  33. package/dist/game/controlGate.d.ts +6 -0
  34. package/dist/game/controlGate.js +30 -1
  35. package/dist/game/defineGame.d.ts +10 -1
  36. package/dist/game/events.d.ts +8 -1
  37. package/dist/game/pixelPerfect.d.ts +25 -0
  38. package/dist/game/pixelPerfect.js +29 -0
  39. package/dist/game/playableGame.d.ts +97 -3
  40. package/dist/game/systemRuntime.js +6 -1
  41. package/dist/input/actionBindings.d.ts +2 -1
  42. package/dist/input/actionBindings.js +10 -8
  43. package/dist/input/actionContexts.d.ts +21 -0
  44. package/dist/input/actionContexts.js +56 -0
  45. package/dist/input/gamepadModel.d.ts +45 -0
  46. package/dist/input/gamepadModel.js +69 -0
  47. package/dist/meta/changelog.d.ts +1 -1
  48. package/dist/meta/changelog.js +103 -1
  49. package/dist/movement/characterController.d.ts +76 -0
  50. package/dist/movement/characterController.js +216 -0
  51. package/dist/movement/freeFlight.d.ts +119 -0
  52. package/dist/movement/freeFlight.js +294 -0
  53. package/dist/movement/playerMovement.d.ts +10 -2
  54. package/dist/movement/playerMovement.js +218 -4
  55. package/dist/movement/serverStep.d.ts +5 -0
  56. package/dist/movement/serverStep.js +6 -0
  57. package/dist/movement/solidObstacles.d.ts +30 -7
  58. package/dist/movement/solidObstacles.js +27 -4
  59. package/dist/multiplayer/poseSyncGate.d.ts +3 -1
  60. package/dist/multiplayer/poseSyncGate.js +9 -1
  61. package/dist/multiplayer/presenceContract.d.ts +6 -0
  62. package/dist/multiplayer/presenceModel.d.ts +2 -0
  63. package/dist/multiplayer/presenceModel.js +11 -0
  64. package/dist/nav/navMesh.d.ts +33 -0
  65. package/dist/nav/navMesh.js +164 -0
  66. package/dist/physics/flightDynamics.d.ts +8 -0
  67. package/dist/physics/kinematicVehicle.d.ts +6 -1
  68. package/dist/physics/physicsBackend.d.ts +191 -0
  69. package/dist/physics/physicsBackend.js +49 -0
  70. package/dist/physics/physicsBackendConformance.d.ts +27 -0
  71. package/dist/physics/physicsBackendConformance.js +117 -0
  72. package/dist/physics/physicsWorldBackend.d.ts +44 -0
  73. package/dist/physics/physicsWorldBackend.js +485 -0
  74. package/dist/physics/worldColliders.d.ts +13 -0
  75. package/dist/physics/worldColliders.js +96 -0
  76. package/dist/random/rng.d.ts +12 -1
  77. package/dist/random/rng.js +20 -3
  78. package/dist/render/environment.d.ts +23 -0
  79. package/dist/render/environment.js +1 -0
  80. package/dist/render/lookPreset.d.ts +11 -5
  81. package/dist/render/lookPreset.js +29 -4
  82. package/dist/render/postProcessing.d.ts +10 -1
  83. package/dist/render/sprite2d.d.ts +38 -0
  84. package/dist/render/sprite2d.js +40 -0
  85. package/dist/runtime/adapter.d.ts +20 -1
  86. package/dist/runtime/adapter.js +8 -1
  87. package/dist/runtime/commandInput.d.ts +15 -0
  88. package/dist/runtime/commandInput.js +13 -0
  89. package/dist/runtime/commandRunner.d.ts +44 -3
  90. package/dist/runtime/commandRunner.js +41 -3
  91. package/dist/runtime/gameContext.js +11 -1
  92. package/dist/runtime/gameContextTypes.d.ts +14 -4
  93. package/dist/runtime/gameRuntime.d.ts +33 -4
  94. package/dist/runtime/gameRuntime.js +26 -5
  95. package/dist/runtime/headlessRunner.d.ts +10 -0
  96. package/dist/runtime/headlessRunner.js +26 -8
  97. package/dist/runtime/hostPolicy.d.ts +3 -2
  98. package/dist/runtime/hostPolicy.js +8 -10
  99. package/dist/runtime/hostedGameRunner.js +34 -6
  100. package/dist/runtime/hostedWorldSession.d.ts +12 -16
  101. package/dist/runtime/hostedWorldSession.js +29 -6
  102. package/dist/runtime/hostedWorldStore.d.ts +20 -0
  103. package/dist/runtime/hostedWorldStore.js +1 -0
  104. package/dist/runtime/inputRecorder.d.ts +30 -0
  105. package/dist/runtime/inputRecorder.js +59 -0
  106. package/dist/runtime/inputSnapshot.d.ts +8 -0
  107. package/dist/runtime/inputSnapshot.js +1 -0
  108. package/dist/runtime/poseInterpolation.d.ts +36 -0
  109. package/dist/runtime/poseInterpolation.js +91 -0
  110. package/dist/runtime/prediction.d.ts +45 -0
  111. package/dist/runtime/prediction.js +62 -0
  112. package/dist/runtime/simContext.d.ts +45 -0
  113. package/dist/runtime/simContext.js +65 -0
  114. package/dist/runtime/simLoop.d.ts +52 -0
  115. package/dist/runtime/simLoop.js +72 -0
  116. package/dist/runtime/simSnapshot.d.ts +32 -0
  117. package/dist/runtime/simSnapshot.js +60 -0
  118. package/dist/runtime/snapshot.d.ts +8 -0
  119. package/dist/runtime/snapshotBuffer.d.ts +33 -0
  120. package/dist/runtime/snapshotBuffer.js +58 -0
  121. package/dist/runtime/transport.d.ts +38 -2
  122. package/dist/runtime/worldChunks.d.ts +2 -0
  123. package/dist/runtime/worldChunks.js +14 -0
  124. package/dist/runtime/worldProjection.d.ts +9 -0
  125. package/dist/runtime/worldProjection.js +17 -1
  126. package/dist/runtime/worldSnapshot.d.ts +6 -0
  127. package/dist/runtime/worldSnapshot.js +3 -0
  128. package/dist/scene/behaviorRuntime.d.ts +19 -1
  129. package/dist/scene/behaviorRuntime.js +55 -4
  130. package/dist/scene/behaviors.d.ts +8 -1
  131. package/dist/scene/builtinSceneKinds.js +10 -0
  132. package/dist/sensor/perception.d.ts +60 -0
  133. package/dist/sensor/perception.js +142 -0
  134. package/dist/settings/graphicsProfile.d.ts +19 -0
  135. package/dist/settings/graphicsProfile.js +36 -0
  136. package/dist/time/accrueSince.d.ts +10 -0
  137. package/dist/time/accrueSince.js +9 -0
  138. package/dist/time/rateWindow.d.ts +16 -0
  139. package/dist/time/rateWindow.js +14 -0
  140. package/dist/time/serverTick.d.ts +21 -3
  141. package/dist/time/serverTick.js +14 -13
  142. package/dist/vfx/particleDirector.d.ts +68 -0
  143. package/dist/vfx/particleDirector.js +81 -0
  144. package/dist/world/buildings.d.ts +12 -0
  145. package/dist/world/buildings.js +43 -3
  146. package/dist/world/cityContent.js +103 -25
  147. package/dist/world/cityKind.d.ts +132 -0
  148. package/dist/world/cityKind.js +315 -15
  149. package/dist/world/features.d.ts +27 -1
  150. package/dist/world/features.js +7 -2
  151. package/dist/world/placement.d.ts +7 -0
  152. package/dist/world/placement.js +2 -0
  153. package/dist/world/placementController.d.ts +1 -1
  154. package/dist/world/placementController.js +7 -0
  155. package/dist/world/raceCircuit.d.ts +168 -0
  156. package/dist/world/raceCircuit.js +797 -0
  157. package/dist/world/terrain.d.ts +5 -1
  158. package/dist/world/terrain.js +5 -0
  159. package/dist/world/territory.d.ts +97 -0
  160. package/dist/world/territory.js +182 -0
  161. package/dist/world.d.ts +5 -3
  162. package/dist/world.js +4 -2
  163. package/package.json +1 -1
  164. package/skills/ce-handoff/SKILL.md +1 -1
  165. package/skills/game-design/SKILL.md +11 -1
  166. package/skills/jgengine/SKILL.md +2 -0
  167. package/skills/jgengine/api.md +98 -22
  168. package/skills/jgengine/capabilities.md +28 -0
  169. package/skills/jgengine-gameplay/SKILL.md +10 -0
  170. package/skills/jgengine-gameplay/api.md +40 -18
  171. package/skills/jgengine-gameplay/capabilities.md +12 -0
  172. package/skills/jgengine-world/SKILL.md +6 -0
  173. package/skills/jgengine-world/api.md +312 -12
  174. package/skills/jgengine-world/capabilities.md +102 -0
@@ -0,0 +1,45 @@
1
+ import type { ActionCodesMap } from "./actionBindings.js";
2
+ /** A serializable gamepad state sampled from the platform input API. */
3
+ export interface GamepadSnapshot {
4
+ id: string;
5
+ axes: number[];
6
+ buttons: {
7
+ pressed: boolean;
8
+ value: number;
9
+ }[];
10
+ connected: boolean;
11
+ }
12
+ /** Names of the built-in controller glyph sets. */
13
+ export type GamepadGlyphName = "xbox" | "playstation" | "nintendo" | "generic";
14
+ /** Button labels used by one controller family. */
15
+ export interface GamepadGlyphSet {
16
+ buttons: readonly string[];
17
+ }
18
+ /** Built-in short labels for the common controller families. */
19
+ export declare const GAMEPAD_GLYPH_SETS: Record<GamepadGlyphName, GamepadGlyphSet>;
20
+ /** Lowercase alias for consumers that prefer data-oriented naming. */
21
+ export declare const gamepadGlyphSets: Record<GamepadGlyphName, GamepadGlyphSet>;
22
+ /** Action bindings whose codes identify gamepad buttons or axes. */
23
+ export type GamepadBindings<TAction extends string = string> = ActionCodesMap<TAction, GamepadCode>;
24
+ /** A gamepad button or signed axis binding code. */
25
+ export type GamepadCode = `pad:${number}` | `padaxis:${number}${"+" | "-"}`;
26
+ /** Deadzone policy applied to gamepad axes. */
27
+ export interface GamepadDeadzone {
28
+ kind: "radial" | "axial";
29
+ inner: number;
30
+ outer: number;
31
+ }
32
+ /** Options for resolving one gamepad snapshot into action state. */
33
+ export interface ResolveGamepadFrameOptions {
34
+ deadzone: GamepadDeadzone;
35
+ curve?: number;
36
+ }
37
+ /** The digital and analog action state produced by a gamepad frame. */
38
+ export interface GamepadFrame {
39
+ held: string[];
40
+ analog: Record<string, number>;
41
+ }
42
+ /** Resolve one sampled gamepad into held actions and shaped analog action values.
43
+ * @capability gamepad-input Resolve sampled gamepad input into actions with deadzones and response curves.
44
+ */
45
+ export declare function resolveGamepadFrame(snapshot: GamepadSnapshot, bindings: GamepadBindings, options: ResolveGamepadFrameOptions): GamepadFrame;
@@ -0,0 +1,69 @@
1
+ /** Built-in short labels for the common controller families. */
2
+ export const GAMEPAD_GLYPH_SETS = {
3
+ xbox: { buttons: ["A", "B", "X", "Y", "LB", "RB", "LT", "RT", "View", "Menu", "L3", "R3", "↑", "↓", "←", "→"] },
4
+ playstation: { buttons: ["Cross", "Circle", "Square", "Triangle", "L1", "R1", "L2", "R2", "Share", "Options", "L3", "R3", "↑", "↓", "←", "→"] },
5
+ nintendo: { buttons: ["B", "A", "Y", "X", "L", "R", "ZL", "ZR", "−", "+", "L3", "R3", "↑", "↓", "←", "→"] },
6
+ generic: { buttons: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "↑", "↓", "←", "→"] },
7
+ };
8
+ /** Lowercase alias for consumers that prefer data-oriented naming. */
9
+ export const gamepadGlyphSets = GAMEPAD_GLYPH_SETS;
10
+ function bindingCodes(codes) {
11
+ if (Array.isArray(codes))
12
+ return codes;
13
+ const modes = codes;
14
+ return [...(modes.hold ?? []), ...(modes.toggle ?? [])];
15
+ }
16
+ function shape(value, deadzone, curve) {
17
+ const magnitude = Math.abs(value);
18
+ if (magnitude <= deadzone.inner)
19
+ return 0;
20
+ const normalized = magnitude >= deadzone.outer ? 1 : (magnitude - deadzone.inner) / (deadzone.outer - deadzone.inner);
21
+ return Math.sign(value) * Math.pow(normalized, curve);
22
+ }
23
+ function readAxis(code) {
24
+ const match = /^padaxis:(\d+)([+-])$/.exec(code);
25
+ return match === null ? null : { index: Number(match[1]), sign: match[2] === "+" ? 1 : -1 };
26
+ }
27
+ /** Resolve one sampled gamepad into held actions and shaped analog action values.
28
+ * @capability gamepad-input Resolve sampled gamepad input into actions with deadzones and response curves.
29
+ */
30
+ export function resolveGamepadFrame(snapshot, bindings, options) {
31
+ if (!snapshot.connected)
32
+ return { held: [], analog: {} };
33
+ const inner = Math.max(0, options.deadzone.inner);
34
+ const outer = Math.max(inner + Number.EPSILON, options.deadzone.outer);
35
+ const curve = options.curve === undefined ? 1 : Math.max(Number.EPSILON, options.curve);
36
+ const rawAxes = snapshot.axes.map((value) => (Number.isFinite(value) ? Math.max(-1, Math.min(1, value)) : 0));
37
+ const axes = rawAxes.map((value) => shape(value, { ...options.deadzone, inner, outer }, curve));
38
+ if (options.deadzone.kind === "radial") {
39
+ const magnitude = Math.hypot(...rawAxes);
40
+ const radial = magnitude <= inner ? 0 : Math.min(1, (magnitude - inner) / (outer - inner));
41
+ const factor = magnitude === 0 ? 0 : (radial * Math.pow(radial, curve - 1)) / magnitude;
42
+ for (let index = 0; index < axes.length; index += 1)
43
+ axes[index] = rawAxes[index] * factor;
44
+ }
45
+ const held = [];
46
+ const analog = {};
47
+ for (const action of Object.keys(bindings)) {
48
+ let actionAnalog = 0;
49
+ let isHeld = false;
50
+ for (const code of bindingCodes(bindings[action])) {
51
+ if (code.startsWith("pad:")) {
52
+ const index = Number(code.slice(4));
53
+ const button = snapshot.buttons[index];
54
+ if (button?.pressed === true)
55
+ isHeld = true;
56
+ actionAnalog = Math.max(actionAnalog, button?.value ?? 0);
57
+ continue;
58
+ }
59
+ const axis = readAxis(code);
60
+ if (axis !== null)
61
+ actionAnalog = Math.max(actionAnalog, Math.max(0, axes[axis.index] * axis.sign));
62
+ }
63
+ if (isHeld || actionAnalog > 0)
64
+ held.push(action);
65
+ if (actionAnalog !== 0)
66
+ analog[action] = actionAnalog;
67
+ }
68
+ return { held, analog };
69
+ }
@@ -1,5 +1,5 @@
1
1
  /** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
2
- export declare const VERSION = "0.17.0";
2
+ export declare const VERSION = "0.18.1";
3
3
  /** One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md). */
4
4
  export interface ChangelogEntry {
5
5
  migrate: readonly string[];
@@ -1,7 +1,109 @@
1
1
  /** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
2
- export const VERSION = "0.17.0";
2
+ export const VERSION = "0.18.1";
3
3
  /** Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`). */
4
4
  export const CHANGELOG = {
5
+ "0.18.1": {
6
+ migrate: [
7
+ "Bump lockstep SDK packages to `^0.18.1`: `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.15.1`; `@jgengine/github` is `0.5.1`.",
8
+ "Declare `topology: \"shared\"` on runtime definitions. Shared Convex hosts migrate the bounded legacy roster into indexed membership and per-player session rows; use `getServerCapacity` for stable lobby subscriptions.",
9
+ "Put every command's read scope on its definition. Omitted scopes now load the actor and no chunks; explicitly request whole-world reads only where needed. Trusted host mutations can compose writes through `helpers.runCommand`.",
10
+ "Handle typed join outcomes (`ok` and `reason`) or use `useServerSession` and `JoinGate`. Pass runtimes to `jgengineCronSpecs`; tick registration requires an actual tick hook.",
11
+ "Declare currency precision with `decimals` and remove game-side cent conversions. Public amounts stay in major units and wallet writes round through integer minor units.",
12
+ "Replace custom presence, chat, territory and tick batching with `createWorldPresenceStore`, chat helpers, chunk territory, and `forEachOnlinePlayer`. Legacy claim backfills must finish before removing their old schema.",
13
+ "`AudioEngine.setListenerPose` now accepts `{ position, forward, up }`. Bare `Vec3` positions remain compatible for one release and use forward `[0, 0, -1]` with up `[0, 1, 0]`.",
14
+ "Hosted world stores are asynchronous. Implement `HostedWorldStore.load()` and `save()` as `Promise`-returning methods and construct persisted sessions with `createHostedWorldSessionAsync`; synchronous tests can keep using `memoryWorldStore`.",
15
+ "Look presets now default to `neutral`. Games that relied on the previous implicit cinematic sky/post stack should set `look: \"photoreal\"` (or keep `look: \"cinematic\"`) explicitly.",
16
+ "`ServerTickPlan.due` is `{ id, runs }[]` instead of a repeated id list. `planServerTick` repeated a system id once per missed interval, and the obvious way to consume that — `due.includes(id)` or a `Set` — ran the system once while the returned anchor had already advanced by every repeat, silently dropping the catch-up work. A host now reads `runs` (or `tickRunCount(plan, id)`) and loops that many times.",
17
+ "`CommandDef.validate` / `CommandDef.apply` take a fourth argument, `nowMs`. Existing implementations that ignore it are unaffected. Commands that took a `now` in their `input` should read this instead — the client supplied the old one.",
18
+ ],
19
+ added: [
20
+ "Indexed shared membership and capacity rows, bounded neighborhood poses with 100 ms server/client gates, durable rate limits, authenticated diagnostics and bounded retention.",
21
+ "Territory footprint planning and atomic placement claims, chat validation and headless chat/join/territory surfaces, elapsed accrual, safe quantities, and player-profile reset through the initializer.",
22
+ "An explicit `--patch` release option.",
23
+ "Added smooth interpolation for remote presence player poses.",
24
+ "WS ping/pong messages and client RTT sampling on the backend object.",
25
+ "Added skin-backed HUD chrome variables, font loading, and the procedural `HudSkinPreview` fixture.",
26
+ "Core gamepad snapshot model, deadzone/curve resolver, and controller glyph labels.",
27
+ "Shell gamepad source polls connected pads, merges analog actions, supports synthetic `?gamepad=1` input, and exposes `ctx.input.rumble`.",
28
+ "Core action context stacks layer gameplay, menus, and passthrough overlays with snapshot/restore; shell action tracking now respects the active context.",
29
+ "Shell audio now supports listener orientation and per-sound Web Audio panners. `setListenerPose` accepts `{ position, forward, up }`; bare positions remain supported for one release.",
30
+ "Core perception tracks deterministic sight, hearing, occlusion, and bounded observation memory for AI.",
31
+ "`syncWorldColliders` now keeps terrain and static object colliders synchronized with a physics backend.",
32
+ "Core decision graphs provide deterministic selector, sequence, condition, action, and utility runtimes with snapshot/restore.",
33
+ "Core behavior descriptors can run registered decision graphs at the interest-scheduler cadence.",
34
+ "Core navigation meshes provide deterministic polygon routing, off-mesh links, closest-point queries, and nav raycasts.",
35
+ "WS sessions now issue resume tickets and retain disconnected memberships for a 15-second grace window, allowing reconnects to rejoin without replacing player state.",
36
+ "Shell entity sprites can play atlas-backed sprite clips while preserving raw texture sprites.",
37
+ "Tilemap layers can render atlas-backed textured tiles as one instanced quad mesh with parallax.",
38
+ "Core pixel-perfect orthographic frustum and pixel-grid camera snapping are available to shell cameras.",
39
+ "Core sprite atlas adapters and deterministic 2D sprite clip playback primitives.",
40
+ "`src/art-direction.md` scaffold and `check-art-direction` gate for created games.",
41
+ "Host-authoritative shell sessions now expose each accepted world frame to the local prediction buffer, reconciling and snapping the possessed pose only when drift exceeds its threshold.",
42
+ "Shared GLTF loader configuration for Draco and KTX2-compressed assets, with CDN defaults and renderer capability detection.",
43
+ "Material map overrides now support metalness, emissive, and height maps on standard and physical materials.",
44
+ "Presentation and editor documents now accept gradient, HDRI/EXR, and cube-map environment sources.",
45
+ "Authored `light` markers now feed bounded point/spot punctual lights with an eight-light default budget and editor warning.",
46
+ "Graphics profiles now resolve per-tier render scale, shadow budgets, culling distance, particles, cascades, and post stages; games can override them with `defineGame({ graphics })`.",
47
+ "`defineGame({ physics: { backend } })` now adopts a capsule character controller for shell/headless movement and registers the backend simulation after movement, including floor collision and step-up behavior.",
48
+ "Added the `physics-probe` dev scene for Rapier crate, ragdoll, and vehicle motion.",
49
+ "Assets now expose import-spec validation and magic-byte classification for supported source files.",
50
+ "Hosted WS sessions now support spectator joins, viewer roles, throttled low-priority snapshot modules, and default command rate limits.",
51
+ "`@jgengine/navbake` bakes deterministic navigation meshes and editor documents can store named bake data.",
52
+ "Core tactical queries derive cover points and score bounded candidate positions.",
53
+ "Added createSnapshotBuffer for delayed snapshot interpolation.",
54
+ "Host-authoritative world replication over WS. Hosted sessions expose revision-cursor pulls, so server subscribers receive compact world diffs and automatically recover with a fresh baseline after missed revisions; viewer-specific projections still receive per-viewer baselines.",
55
+ "Animation graph — `ModelAnimationConfig.graph` and `@jgengine/core/anim/animGraph`. A serializable, layered animation state machine: `clip`, `blend1D`, and `blend2D` states, transitions as data (parameter comparisons, consumed triggers, exit times, crossfade durations), masked and additive layers, and clip events. `createAnimGraphRuntime` is headless and owns every clip's playback time and weight, so the shell's mixer only seeks and weights actions (`useModelAnimation` takes the graph path when `graph` is set) and a headless host or test advances the same graph without three.js. The shell feeds the entity's smoothed ground speed as `speed`, merges parameters from the entity blackboard key `ANIM_PARAMS_KEY`, arms triggers from `entity.animation` / `combat.hitReaction` / `entity.died`, and emits the new `animation.event` for clip events (foot plants, hit frames). `locomotionGraph()` builds the previous idle/walk/run plus one-shot behavior as one authored graph.",
56
+ "`PhysicsBackend` — one interface over interchangeable rigid-body solvers. `@jgengine/core/physics/physicsBackend` defines bodies (`BodyDesc` with a box / sphere / capsule / convex / trimesh / heightfield shape union, quaternion rotation, angular velocity, layers and mask, CCD flag), joints, `raycast`, `shapecast`, `overlap`, contact events, and `snapshot`/`restore` with stable handles; `capabilities` says what a backend really does. `createPhysicsWorldBackend` puts the zero-dependency `PhysicsWorld` behind it (translation-only, non-box shapes collide as their bounding box, warned once), and `runPhysicsBackendConformance` is the behavioral contract any adapter runs. `createCharacterController` (`@jgengine/core/movement/characterController`) is the first consumer: a capsule collide-and-slide controller with step-up, slope limit, ceiling test, crouch with headroom check, ground snapping, and moving-platform carry, pure over any backend and retunable during play.",
57
+ "Fixed-step simulation with render interpolation — `defineGame({ simulation: { hz } })` and `ctx.sim`. Every driver (shell frame, HUD-only, headless runner, authoritative host) now steps through one `SimLoop` (`@jgengine/core/runtime/simLoop`): a fixed `hz` accumulates real time and runs equal-dt steps behind a catch-up cap (`maxCatchUpSteps`, default 5) instead of spiralling; the default stays `\"variable\"` (one step per frame) so existing games are unchanged until they opt in. Under a fixed rate the shell renders entities at `ctx.sim.renderPose(id)` — the pose interpolated between the previous and current step (`@jgengine/core/runtime/poseInterpolation`), with teleports past `snapDistance` snapping — so a 20 Hz sim no longer stutters. `ctx.sim.tick()` counts steps and `InputFrame.tick` carries it over the wire; `ctx.sim.addStage({ phase, run })` inserts per-step work before movement, after movement, or after `onTick` (the hook a physics backend or netcode buffer registers through). System fixed-rate groups gain the same substep cap. For replay and rollback: `seededRng` streams expose `state()`/`restore()` (`rngStateOf`/`restoreRng` for any `ctx.rng`), `createSimSnapshotRegistry`/`createContextSimSnapshot` capture world, clock, loop, and rng cursor together, `createInputRecorder` logs frames by tick, and `HeadlessRunner` gains `snapshot()`/`restore()`/`tick()` — a test proves that replaying recorded input from a snapshot reproduces every entity pose.",
58
+ "City and street races share one generator. `extractCircuitRoute` lifts a sealed-off race lap from a city street network; city rules, editor baking, playground modes, and the shell renderer carry the same street dials through the resulting race surface and junction dressing.",
59
+ "`ctx.particles` — the game-reachable particle seam. Game logic queues one-shot bursts (`ctx.particles.burst(config, count)`) and keyed standing emitters (`attach`/`retune`/`detach`, with `follow` to track a scene entity) as plain serializable data (`@jgengine/core/vfx/particleDirector`); the shell auto-mounts a renderer that turns them into pooled GPU point clouds via the existing `createParticleSystem`/`ParticleField`, caps pool sizes by graphics quality (low 128 / medium 256 / high 512 per effect, degrading density instead of dropping effects), and reaps finished bursts. Dust, sparks, exhaust, and debris no longer require forking the shell; headless tests assert the requested effects straight off the director.",
60
+ "`resolveSourceWalkerStep(source, cache, position, stepX, stepZ, options)` (`@jgengine/core/movement/solidObstacles`) — `resolveWalkerStep` with no `GameContext`, for movement rules that live in a pure package with no React or three.js. `SolidObstacleSource`, `ObstacleReachCache`, `createObstacleReachCache`, `sourceObstacleReach`, `sourceObstaclesNear`, and `slideStep` are public alongside it instead of `@internal`, so a game no longer has to push the resolver up into its app layer and inject it back down. `resolveWalkerStep`'s doc now states that it already resolves X and Z separately, so a caller doing its own axis-split must hand it one axis per call.",
61
+ "`tickRunCount(plan, id)` (`@jgengine/core/time/serverTick`) — how many runs a plan owes a system, or 0 when it is not due.",
62
+ "`grass()` exposes the clump/lighting knobs its shader already had. `GrassEnvironmentConfig` gains optional `bladeBend`, `tuftRadius`, `colorVariation`, and `normalLift`, forwarded by the shell's environment renderer — a world descriptor can now author bushy wind-combed scrub instead of the fixed lawn-blade look; defaults unchanged.",
63
+ "`TerrainDetailConfig.sweeps` — the detail shader's two macro colour sweeps (sun-dried patches, cooler wet pockets) take per-world RGB multipliers instead of hardcoded meadow tints. The old green-leaning \"lush pocket\" default painted moss onto arid/ashen biomes; defaults are unchanged, so a desert world should set warm/neutral `sweeps` explicitly.",
64
+ "`@jgengine/rapier` — a Rapier-backed `PhysicsBackend` adapter with native capsules, shapecasts, rotation, CCD, joints, and collision queries.",
65
+ ],
66
+ changed: [
67
+ "Hosted game runner queues authoritative inputs by simulation tick and applies the in-force frame each fixed step.",
68
+ "Create scaffolds now use a 60 Hz simulation with interpolation enabled by default.",
69
+ "Rigged `ModelConfig` entries can opt into shell foot IK with named thigh/shin/foot bones; the driver raycasts the shared scene terrain, solves each chain, and fades corrections while airborne.",
70
+ "Animation graph clips may carry root-bone position tracks; states with `rootMotion: true` return the sampled displacement and the shell applies it to the entity while restoring the root bone's bind translation.",
71
+ "Added renderer-free two-bone, FABRIK, and look-at inverse-kinematics solvers to `@jgengine/core`.",
72
+ "Shell model animation now routes states and one-shots through the shared animation graph runtime.",
73
+ "CI now clones the external games repository with `GAMES_CLONE_TOKEN` and runs the game shape, content, front-end, art-direction, and feel gates against it; those gates fail loudly in CI when the checkout is missing while remaining no-ops locally.",
74
+ "`jgengine create` now accepts `--player`, `--ground`, and `--scene` so generated games can choose their model, terrain ground, and empty or starter scene.",
75
+ "Directional shadow frustum config actually reaches the GPU. `shadowCameraSize` on a single (non-cascaded) shadow light was silently inert: the shell wrote the bounds through R3F pierced props and nothing refreshed the shadow camera's projection matrix, so three rendered depth with its default ~10-unit box at the world origin — the reason no open-world game had cast shadows away from spawn. The shell now syncs the projection (and reallocates the shadow map when `shadowMapSize` changes). `bloom.threshold`'s doc also now states it measures raw HDR before the exposure stage.",
76
+ "CSM (`cascades > 1`) no longer over-lights streamed meshes or clobbers material shaders. Cascade scoping is injected per material, but the scan ran every 30 frames, so a model that streamed in rendered lit by every cascade light at once (a cascades× sun) until the next scan — a sub-second flash on desktop, the whole shot on capture rigs. The scan now runs every frame behind a WeakSet, and `setupMaterial` chains with a material's own `onBeforeCompile` (rim light, detail maps) instead of replacing it.",
77
+ "The host's wall clock reaches the runtime hooks. `RuntimeInitContext` (so `onInit`, `onNewPlayer`, and `onTick` alike) carries `nowMs`, the host timestamp for that call. Anything keyed to real time — a UTC date rollover, a `lastTickAt` anchor read by non-tick code, an RNG seed or `createdAt` stamp — reads it rather than accumulating `dtSeconds` against an epoch persisted in the snapshot, which drifts by whatever the tick loses to stalls and `maxCatchUp` clamping. `GameRuntime.hydrate` takes an optional `nowMs`; `tick`, `joinPlayer`, and `runCommand` take an optional trailing `nowMs`. All default to `Date.now()`, and the shipped WS and Convex hosts pass the timestamp they already computed to plan the tick.",
78
+ "Imported models cast and receive shadows. `cloneModelScene` never set `castShadow`/`receiveShadow` on a GLB's meshes (only the magenta fallback box had them), so every catalog model in every game rendered shadowless however the lights were configured. Cloned models now default to `ModelConfig.shadows: \"both\"`; set `\"cast\"`, `\"receive\"`, or `\"none\"` per model (foliage cards, decals, viewmodels).",
79
+ "`jgengine create` installs `game-design` and `jgengine-ui` by default. The minimal skill set was intake, editor, and verify, so a real project never saw the greenfield design contract or the UI art-direction template unless someone ran `jgengine skills --all`; `MINIMAL_GAME_SKILLS` now carries both.",
80
+ ],
81
+ removed: [],
82
+ },
83
+ "0.18.0": {
84
+ migrate: [
85
+ "Bump lockstep SDK packages to `^0.18.0`: `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.15.0`; `@jgengine/github` is `0.5.0`.",
86
+ "Register the presence reaper and redeploy your schema. `jgPoses` gains `sessionId` / `kind` / `label` / `revokedAt` (all optional) and a `by_updated` index, and `createPresenceFunctions` now also returns `reapIdlePresence`. Re-export it (`export const { list, sync, leave, reapIdlePresence } = createPresenceFunctions(...)`) and add `crons.interval(\"jg presence reap\", { seconds: 60 }, internal.presence.reapIdlePresence, {})`. Without it nothing deletes the row of a player who closed the tab — `list` filtered stale rows at read time but the table grew forever. `jgengineCronSpecs()` carries the new entry, and every spec now names the `module` (`\"runtime\"` / `\"presence\"`) it is exported from.",
87
+ "`presence.sync` returns `{ pose, lastSeenAt, displaced }` instead of `null`, and `presence.leave` marks the row revoked instead of deleting it (the reaper collects it once idle). A caller that ignored the return value is unaffected.",
88
+ ],
89
+ added: [
90
+ "Polygon navigation meshes — `NavMeshData`, adjacency building, polygon A* paths, closest-point, and nav raycasts (`@jgengine/core/nav/navMesh`).",
91
+ "`createPerception` (`@jgengine/core/sensor/perception`) tracks sight, sound, and damage stimuli per observer, with cone/range/occlusion checks, linearly decaying memory, retuning, and snapshot/restore.",
92
+ "`syncWorldColliders(backend, ctx)` mirrors static scene-object colliders and the sampled ground heightfield into a physics backend, updating on object spawn, despawn, and pose changes.",
93
+ "Physics-backed player movement — `defineGame({ physics: { backend } })` installs the backend's `afterMovement` simulation stage and makes the shell/headless walk controller use a capsule for gravity, shapecast collision, slopes, and step-up, while retaining the existing input and impulse feel.",
94
+ "`createPresenceFunctions` gains the policy hooks the pose lane was missing — `resolveSpawn(ctx, { serverId, userId, kind })` for where a session with no row starts (async and ctx-bearing, so a spawn can read the world, replacing a hardcoded origin), `poseRules` as a function of the actor `kind` so an agent need not obey a human's speed cap, `idleCutoffMs` / `maxReapPerRun` for the reaper, and optional `sessionId` on `sync` / `leave`: a second tab is a second row, syncing revokes the others, and the displaced session learns it on its next call through `displaced: true`.",
95
+ "A command declares what it touches, so `runCommand` hydrates that and nothing else. `CommandDef` gains an optional `scope(input, actorUserId)` returning `{ players?, chunkKeys? }`, evaluated before hydration; the registered `runCommand` mutation also accepts an explicit `scope` argument, which wins over the declared one. A game that never sets either keeps the whole-world default. `ServerLoopHooks.joinScope(userId, isNew)` does the same for `onNewPlayer`.",
96
+ "`GameRuntime.commandScope` / `GameRuntime.joinScope` expose those declarations to any host, alongside the pure helpers `resolveCommandScope` / `scopeWithActor` / `commandScopeEscape` (`@jgengine/core/runtime/commandRunner`).",
97
+ ],
98
+ changed: [
99
+ "Rigged `ModelConfig` entries can opt into shell foot IK with named thigh/shin/foot bones; the driver raycasts the shared scene terrain, solves each chain, and fades corrections while airborne.",
100
+ "Animation graph clips may carry root-bone position tracks; states with `rootMotion: true` return the sampled displacement and the shell applies it to the entity while restoring the root bone's bind translation.",
101
+ "`presence.sync` accepts a call with no `pose` at all, which holds the stored position and moves only the liveness stamp. `createConvexPresenceSync` sends one itself when the pose gate has suppressed writes for 10s, so a standing player keeps their row alive without a pose write — and without the reaper collecting a live session.",
102
+ "`PresencePoseRow` carries optional `sessionId` / `kind` / `label`, so a nameplate reads off the presence row instead of joining against users per frame.",
103
+ "`runCommand` no longer reads the whole world on every command. `LoadSnapshotScope` landed in 0.17 but only `helpers.loadSnapshot` accepted one, so the supported way to run a command — the registered `runCommand` mutation, a one-line delegation to `helpers.runCommand` — hydrated every member profile and every chunk however narrow the command was, and a game could only get bounded hydration by reimplementing the mutation. `runCommand`, `joinServer`, and `leaveServer` now hydrate a scope; a command whose `apply` dirties a player or chunk its scope never named is refused rather than persisted, since writing an unhydrated chunk overwrites the stored one with a blank. `flushSave` / `flushServerIfDue` / `tickActiveServers` / `flushDirtyServers` still hydrate everything, which is correct for what they write.",
104
+ ],
105
+ removed: [],
106
+ },
5
107
  "0.17.0": {
6
108
  migrate: [
7
109
  "Bump lockstep SDK packages to `^0.17.0`: `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.14.0`; `@jgengine/github` is `0.4.0`.",
@@ -0,0 +1,76 @@
1
+ import type { BodyHandle, BodyShape, PhysicsBackend, PhysicsVec3 } from "../physics/physicsBackend.js";
2
+ /** Capsule dimensions and the walk rules a controller applies; every field is retunable during play. */
3
+ export interface CharacterControllerConfig {
4
+ radius: number;
5
+ /** Standing height from feet to crown. */
6
+ height: number;
7
+ /** Height the feet may pop up to clear a ledge in one move. Default 0.35. */
8
+ stepHeight?: number;
9
+ /** Steepest walkable slope in degrees; steeper surfaces are walls to slide along. Default 50. */
10
+ maxSlopeDeg?: number;
11
+ /** Gap kept between the capsule and any surface so casts never start inside geometry. Default 0.02. */
12
+ skinWidth?: number;
13
+ /** Height while crouched. Default `height * 0.6`. */
14
+ crouchHeight?: number;
15
+ /** How far below the feet the ground may be while still counting as grounded (stairs down, small bumps). Default 0.3. */
16
+ snapDistance?: number;
17
+ /** Slide iterations per move. Default 4. */
18
+ maxSlides?: number;
19
+ /** Collision mask for the controller's casts. Default all layers. */
20
+ mask?: number;
21
+ }
22
+ /** The controller's serializable state. `position` is the feet point; the capsule stands above it. */
23
+ export interface CharacterControllerState {
24
+ position: [number, number, number];
25
+ verticalVelocity: number;
26
+ grounded: boolean;
27
+ groundNormal: [number, number, number];
28
+ groundBody: BodyHandle | null;
29
+ crouching: boolean;
30
+ }
31
+ /** One step of intent for {@link CharacterController.move}. */
32
+ export interface CharacterMoveInput {
33
+ /** Desired horizontal displacement this step, world units. */
34
+ motion: PhysicsVec3;
35
+ dt: number;
36
+ /** Downward acceleration applied to `verticalVelocity`. Default 0 (caller integrates vertical motion itself). */
37
+ gravity?: number;
38
+ /** Upward velocity to set this step; ignored unless grounded. */
39
+ jumpVelocity?: number;
40
+ /** Request crouch (`true`) or stand (`false`); standing waits until there is headroom. */
41
+ crouch?: boolean;
42
+ /** Body the controller itself is represented by, excluded from its own casts. */
43
+ self?: BodyHandle;
44
+ }
45
+ /** What one move did. */
46
+ export interface CharacterMoveResult {
47
+ moved: [number, number, number];
48
+ grounded: boolean;
49
+ groundNormal: [number, number, number];
50
+ hitWall: boolean;
51
+ hitCeiling: boolean;
52
+ steppedUp: boolean;
53
+ /** Set when a stand request was refused for lack of headroom. */
54
+ crouchBlocked: boolean;
55
+ }
56
+ /** The controller handle: config, state, and the per-step move. */
57
+ export interface CharacterController {
58
+ config(): Readonly<Required<CharacterControllerConfig>>;
59
+ retune(next: Partial<CharacterControllerConfig>): void;
60
+ state(): CharacterControllerState;
61
+ snapshot(): CharacterControllerState;
62
+ restore(next: CharacterControllerState): void;
63
+ /** Current capsule as a backend shape (crouch-aware). */
64
+ shape(): BodyShape;
65
+ /** Capsule center for the current state. */
66
+ center(): [number, number, number];
67
+ move(backend: PhysicsBackend, input: CharacterMoveInput): CharacterMoveResult;
68
+ }
69
+ /**
70
+ * Collide-and-slide capsule controller over any {@link PhysicsBackend}: horizontal slide along walls, step-up over
71
+ * ledges, slope limit, ceiling test, crouch with headroom check, ground snapping, and moving-platform carry read from
72
+ * the ground body's velocity. Pure over the backend's `shapecast`/`overlap`; the caller owns input and gravity policy.
73
+ *
74
+ * @capability character-controller capsule walk over the physics backend — slide, step-up, slopes, crouch, platforms
75
+ */
76
+ export declare function createCharacterController(initial: CharacterControllerConfig): CharacterController;
@@ -0,0 +1,216 @@
1
+ function resolve(config) {
2
+ return {
3
+ radius: config.radius,
4
+ height: config.height,
5
+ stepHeight: config.stepHeight ?? 0.35,
6
+ maxSlopeDeg: config.maxSlopeDeg ?? 50,
7
+ skinWidth: config.skinWidth ?? 0.02,
8
+ crouchHeight: config.crouchHeight ?? config.height * 0.6,
9
+ snapDistance: config.snapDistance ?? 0.3,
10
+ maxSlides: config.maxSlides ?? 4,
11
+ mask: config.mask ?? 0xffffffff,
12
+ };
13
+ }
14
+ /**
15
+ * Collide-and-slide capsule controller over any {@link PhysicsBackend}: horizontal slide along walls, step-up over
16
+ * ledges, slope limit, ceiling test, crouch with headroom check, ground snapping, and moving-platform carry read from
17
+ * the ground body's velocity. Pure over the backend's `shapecast`/`overlap`; the caller owns input and gravity policy.
18
+ *
19
+ * @capability character-controller capsule walk over the physics backend — slide, step-up, slopes, crouch, platforms
20
+ */
21
+ export function createCharacterController(initial) {
22
+ let cfg = resolve(initial);
23
+ let state = {
24
+ position: [0, 0, 0],
25
+ verticalVelocity: 0,
26
+ grounded: false,
27
+ groundNormal: [0, 1, 0],
28
+ groundBody: null,
29
+ crouching: false,
30
+ };
31
+ function currentHeight() {
32
+ return state.crouching ? cfg.crouchHeight : cfg.height;
33
+ }
34
+ function shapeFor(height) {
35
+ const halfHeight = Math.max(0, height / 2 - cfg.radius);
36
+ return { kind: "capsule", radius: cfg.radius, halfHeight };
37
+ }
38
+ function centerFor(position, height) {
39
+ return [position[0], position[1] + height / 2, position[2]];
40
+ }
41
+ function walkable(normal) {
42
+ return normal[1] >= Math.cos((cfg.maxSlopeDeg * Math.PI) / 180);
43
+ }
44
+ function cast(backend, from, height, motion, self) {
45
+ return backend.shapecast({
46
+ shape: shapeFor(height),
47
+ position: centerFor(from, height),
48
+ motion,
49
+ mask: cfg.mask,
50
+ ...(self === undefined ? {} : { exclude: self }),
51
+ });
52
+ }
53
+ /** Move along `motion`, stopping `skinWidth` short of the first hit. Returns the fraction travelled and the hit. */
54
+ function sweep(backend, pos, height, motion, self) {
55
+ const len = Math.hypot(motion[0], motion[1], motion[2]);
56
+ if (!(len > 1e-9))
57
+ return { fraction: 1, hit: null };
58
+ const hit = cast(backend, pos, height, motion, self);
59
+ if (hit === null) {
60
+ pos[0] += motion[0];
61
+ pos[1] += motion[1];
62
+ pos[2] += motion[2];
63
+ return { fraction: 1, hit: null };
64
+ }
65
+ const back = cfg.skinWidth / len;
66
+ const fraction = Math.max(0, hit.toi - back);
67
+ pos[0] += motion[0] * fraction;
68
+ pos[1] += motion[1] * fraction;
69
+ pos[2] += motion[2] * fraction;
70
+ return { fraction, hit };
71
+ }
72
+ return {
73
+ config: () => cfg,
74
+ retune(next) {
75
+ cfg = resolve({ ...cfg, ...next });
76
+ },
77
+ state: () => state,
78
+ snapshot: () => ({
79
+ position: [state.position[0], state.position[1], state.position[2]],
80
+ verticalVelocity: state.verticalVelocity,
81
+ grounded: state.grounded,
82
+ groundNormal: [state.groundNormal[0], state.groundNormal[1], state.groundNormal[2]],
83
+ groundBody: state.groundBody,
84
+ crouching: state.crouching,
85
+ }),
86
+ restore(next) {
87
+ state = {
88
+ position: [next.position[0], next.position[1], next.position[2]],
89
+ verticalVelocity: next.verticalVelocity,
90
+ grounded: next.grounded,
91
+ groundNormal: [next.groundNormal[0], next.groundNormal[1], next.groundNormal[2]],
92
+ groundBody: next.groundBody,
93
+ crouching: next.crouching,
94
+ };
95
+ },
96
+ shape: () => shapeFor(currentHeight()),
97
+ center: () => centerFor(state.position, currentHeight()),
98
+ move(backend, input) {
99
+ const { dt, self } = input;
100
+ const result = {
101
+ moved: [0, 0, 0],
102
+ grounded: false,
103
+ groundNormal: [0, 1, 0],
104
+ hitWall: false,
105
+ hitCeiling: false,
106
+ steppedUp: false,
107
+ crouchBlocked: false,
108
+ };
109
+ const start = [state.position[0], state.position[1], state.position[2]];
110
+ const pos = [start[0], start[1], start[2]];
111
+ if (input.crouch === true && !state.crouching) {
112
+ state.crouching = true;
113
+ }
114
+ else if (input.crouch === false && state.crouching) {
115
+ const standing = backend.overlap({
116
+ shape: shapeFor(cfg.height),
117
+ position: centerFor(pos, cfg.height),
118
+ mask: cfg.mask,
119
+ ...(self === undefined ? {} : { exclude: self }),
120
+ });
121
+ if (standing.length === 0)
122
+ state.crouching = false;
123
+ else
124
+ result.crouchBlocked = true;
125
+ }
126
+ const height = currentHeight();
127
+ let carry = [0, 0, 0];
128
+ if (state.grounded && state.groundBody !== null) {
129
+ const ground = backend.body(state.groundBody);
130
+ if (ground !== null)
131
+ carry = [ground.velocity[0] * dt, ground.velocity[1] * dt, ground.velocity[2] * dt];
132
+ }
133
+ let remaining = [input.motion[0] + carry[0], 0, input.motion[2] + carry[2]];
134
+ for (let i = 0; i < cfg.maxSlides; i += 1) {
135
+ const { hit, fraction } = sweep(backend, pos, height, remaining, self);
136
+ if (hit === null)
137
+ break;
138
+ const left = [remaining[0] * (1 - fraction), remaining[1] * (1 - fraction), remaining[2] * (1 - fraction)];
139
+ const dot = left[0] * hit.normal[0] + left[1] * hit.normal[1] + left[2] * hit.normal[2];
140
+ const slid = [
141
+ left[0] - hit.normal[0] * dot,
142
+ left[1] - hit.normal[1] * dot,
143
+ left[2] - hit.normal[2] * dot,
144
+ ];
145
+ if (!walkable(hit.normal)) {
146
+ result.hitWall = true;
147
+ slid[1] = 0;
148
+ if (cfg.stepHeight > 0 && !result.steppedUp && tryStepUp(backend, pos, height, remaining, self)) {
149
+ result.steppedUp = true;
150
+ result.hitWall = false;
151
+ break;
152
+ }
153
+ }
154
+ if (Math.hypot(slid[0], slid[1], slid[2]) < 1e-6)
155
+ break;
156
+ remaining = slid;
157
+ }
158
+ if (input.gravity !== undefined)
159
+ state.verticalVelocity -= input.gravity * dt;
160
+ if (input.jumpVelocity !== undefined && state.grounded) {
161
+ state.verticalVelocity = input.jumpVelocity;
162
+ state.grounded = false;
163
+ }
164
+ const wasGrounded = state.grounded;
165
+ let vertical = state.verticalVelocity * dt + carry[1];
166
+ if (wasGrounded && vertical <= 0)
167
+ vertical -= cfg.snapDistance;
168
+ const { hit: verticalHit } = sweep(backend, pos, height, [0, vertical, 0], self);
169
+ if (verticalHit !== null && vertical > 0) {
170
+ result.hitCeiling = true;
171
+ state.verticalVelocity = 0;
172
+ state.grounded = false;
173
+ state.groundBody = null;
174
+ }
175
+ else if (verticalHit !== null && walkable(verticalHit.normal)) {
176
+ state.grounded = true;
177
+ state.groundNormal = [verticalHit.normal[0], verticalHit.normal[1], verticalHit.normal[2]];
178
+ state.groundBody = verticalHit.body;
179
+ if (state.verticalVelocity < 0)
180
+ state.verticalVelocity = 0;
181
+ // A contact at t=0 means touching or already inside; lift by the skin so the next casts start clear.
182
+ if (verticalHit.toi <= 0)
183
+ pos[1] += cfg.skinWidth;
184
+ }
185
+ else {
186
+ state.grounded = false;
187
+ state.groundBody = null;
188
+ state.groundNormal = [0, 1, 0];
189
+ }
190
+ state.position = pos;
191
+ result.moved = [pos[0] - start[0], pos[1] - start[1], pos[2] - start[2]];
192
+ result.grounded = state.grounded;
193
+ result.groundNormal = [state.groundNormal[0], state.groundNormal[1], state.groundNormal[2]];
194
+ return result;
195
+ },
196
+ };
197
+ function tryStepUp(backend, pos, height, motion, self) {
198
+ const probe = [pos[0], pos[1], pos[2]];
199
+ const up = sweep(backend, probe, height, [0, cfg.stepHeight, 0], self);
200
+ if (up.hit !== null && up.fraction <= 0)
201
+ return false;
202
+ const forward = sweep(backend, probe, height, motion, self);
203
+ if (forward.hit !== null && forward.fraction < 0.05)
204
+ return false;
205
+ const drop = -(probe[1] - pos[1]);
206
+ const down = sweep(backend, probe, height, [0, drop, 0], self);
207
+ if (down.hit === null || !walkable(down.hit.normal))
208
+ return false;
209
+ if (probe[1] <= pos[1] + 1e-6)
210
+ return false;
211
+ pos[0] = probe[0];
212
+ pos[1] = probe[1];
213
+ pos[2] = probe[2];
214
+ return true;
215
+ }
216
+ }