@jgengine/core 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/CHANGELOG.md +265 -2
  2. package/dist/ai/decisionGraph.d.ts +96 -0
  3. package/dist/ai/decisionGraph.js +212 -0
  4. package/dist/ai/groupAssist.d.ts +10 -0
  5. package/dist/ai/groupAssist.js +11 -0
  6. package/dist/ai/mobBrainGraph.d.ts +3 -0
  7. package/dist/ai/mobBrainGraph.js +25 -0
  8. package/dist/ai/pursuit.d.ts +1 -1
  9. package/dist/ai/pursuit.js +1 -1
  10. package/dist/ai/tacticalQueries.d.ts +30 -0
  11. package/dist/ai/tacticalQueries.js +45 -0
  12. package/dist/ai/threat.d.ts +8 -0
  13. package/dist/ai/threat.js +10 -0
  14. package/dist/anim/animGraph.d.ts +155 -0
  15. package/dist/anim/animGraph.js +414 -0
  16. package/dist/anim/boneTexture.d.ts +55 -0
  17. package/dist/anim/boneTexture.js +43 -0
  18. package/dist/anim/footPlacement.d.ts +57 -0
  19. package/dist/anim/footPlacement.js +67 -0
  20. package/dist/anim/ikSolver.d.ts +51 -0
  21. package/dist/anim/ikSolver.js +124 -0
  22. package/dist/anim/locomotionGraph.d.ts +35 -0
  23. package/dist/anim/locomotionGraph.js +62 -0
  24. package/dist/assets/spriteAtlas.d.ts +21 -0
  25. package/dist/assets/spriteAtlas.js +103 -0
  26. package/dist/audio/audioFalloff.d.ts +12 -0
  27. package/dist/audio/doppler.d.ts +21 -0
  28. package/dist/audio/doppler.js +32 -0
  29. package/dist/audio/engineLayers.d.ts +83 -0
  30. package/dist/audio/engineLayers.js +115 -0
  31. package/dist/combat/abilityKit.d.ts +1 -0
  32. package/dist/combat/abilityKit.js +1 -0
  33. package/dist/combat/death.d.ts +7 -0
  34. package/dist/combat/shotOrigin.d.ts +6 -0
  35. package/dist/combat/shotOrigin.js +6 -0
  36. package/dist/combat/weaponFeedback.d.ts +36 -0
  37. package/dist/combat/weaponFeedback.js +42 -0
  38. package/dist/combat/weaponHandling.d.ts +161 -0
  39. package/dist/combat/weaponHandling.js +202 -0
  40. package/dist/combat/weaponPresentation.d.ts +115 -0
  41. package/dist/combat/weaponPresentation.js +96 -0
  42. package/dist/devtools/warnOnce.d.ts +8 -0
  43. package/dist/devtools/warnOnce.js +18 -0
  44. package/dist/economy/currency.d.ts +16 -2
  45. package/dist/economy/currency.js +55 -14
  46. package/dist/economy/wallet.d.ts +5 -4
  47. package/dist/economy/wallet.js +8 -4
  48. package/dist/editor/commandApply.js +1 -0
  49. package/dist/editor/commands.d.ts +4 -1
  50. package/dist/editor/document.d.ts +2 -1
  51. package/dist/editor/document.js +92 -1
  52. package/dist/editor/environment.d.ts +3 -0
  53. package/dist/editor/environment.js +24 -0
  54. package/dist/editor/index.d.ts +4 -2
  55. package/dist/editor/index.js +1 -1
  56. package/dist/editor/types.d.ts +20 -0
  57. package/dist/faction/reputation.d.ts +6 -0
  58. package/dist/faction/reputation.js +11 -0
  59. package/dist/game/cameraConfig.d.ts +59 -1
  60. package/dist/game/chat.d.ts +22 -1
  61. package/dist/game/chat.js +48 -19
  62. package/dist/game/controlGate.d.ts +6 -0
  63. package/dist/game/controlGate.js +30 -1
  64. package/dist/game/defineGame.d.ts +10 -1
  65. package/dist/game/events.d.ts +14 -1
  66. package/dist/game/lootTable.d.ts +4 -0
  67. package/dist/game/lootTable.js +16 -0
  68. package/dist/game/pixelPerfect.d.ts +25 -0
  69. package/dist/game/pixelPerfect.js +29 -0
  70. package/dist/game/playableGame.d.ts +167 -6
  71. package/dist/game/systemRuntime.js +6 -1
  72. package/dist/game/toasts.d.ts +8 -0
  73. package/dist/game/toasts.js +7 -0
  74. package/dist/game/vfxInstance.d.ts +9 -0
  75. package/dist/game/vfxInstance.js +12 -1
  76. package/dist/game/viewports.d.ts +43 -0
  77. package/dist/game/viewports.js +46 -0
  78. package/dist/game/worldItem.d.ts +5 -1
  79. package/dist/game/worldItem.js +0 -1
  80. package/dist/input/actionBindings.d.ts +17 -2
  81. package/dist/input/actionBindings.js +24 -11
  82. package/dist/input/actionContexts.d.ts +45 -0
  83. package/dist/input/actionContexts.js +113 -0
  84. package/dist/input/axisInput.d.ts +4 -0
  85. package/dist/input/axisInput.js +4 -0
  86. package/dist/input/axisShaper.d.ts +73 -0
  87. package/dist/input/axisShaper.js +105 -0
  88. package/dist/input/gamepadModel.d.ts +79 -0
  89. package/dist/input/gamepadModel.js +108 -0
  90. package/dist/input/haptics.d.ts +50 -0
  91. package/dist/input/haptics.js +74 -0
  92. package/dist/input/inputBuffer.d.ts +40 -0
  93. package/dist/input/inputBuffer.js +59 -0
  94. package/dist/input/lookChannel.d.ts +10 -0
  95. package/dist/input/lookChannel.js +6 -0
  96. package/dist/input/pointer.d.ts +2 -0
  97. package/dist/input/pointer.js +4 -0
  98. package/dist/item/affix.d.ts +6 -0
  99. package/dist/item/affix.js +6 -0
  100. package/dist/meta/changelog.d.ts +1 -1
  101. package/dist/meta/changelog.js +196 -1
  102. package/dist/movement/characterController.d.ts +76 -0
  103. package/dist/movement/characterController.js +216 -0
  104. package/dist/movement/freeFlight.d.ts +119 -0
  105. package/dist/movement/freeFlight.js +294 -0
  106. package/dist/movement/glideModel.d.ts +3 -0
  107. package/dist/movement/glideModel.js +6 -0
  108. package/dist/movement/grappleSwing.d.ts +2 -0
  109. package/dist/movement/grappleSwing.js +5 -1
  110. package/dist/movement/movementModel.d.ts +39 -0
  111. package/dist/movement/movementModel.js +62 -19
  112. package/dist/movement/movementProbe.d.ts +51 -0
  113. package/dist/movement/movementProbe.js +119 -0
  114. package/dist/movement/playerMovement.d.ts +30 -3
  115. package/dist/movement/playerMovement.js +304 -7
  116. package/dist/movement/serverStep.d.ts +5 -0
  117. package/dist/movement/serverStep.js +6 -0
  118. package/dist/movement/solidObstacles.d.ts +39 -8
  119. package/dist/movement/solidObstacles.js +71 -6
  120. package/dist/movement/voxelController.js +7 -7
  121. package/dist/multiplayer/poseSyncGate.d.ts +3 -1
  122. package/dist/multiplayer/poseSyncGate.js +9 -1
  123. package/dist/multiplayer/presenceContract.d.ts +6 -0
  124. package/dist/multiplayer/presenceModel.d.ts +2 -0
  125. package/dist/multiplayer/presenceModel.js +11 -0
  126. package/dist/nav/navFromEnvironment.d.ts +9 -3
  127. package/dist/nav/navFromEnvironment.js +18 -12
  128. package/dist/nav/navMesh.d.ts +74 -0
  129. package/dist/nav/navMesh.js +570 -0
  130. package/dist/physics/aircraftDynamics.d.ts +390 -0
  131. package/dist/physics/aircraftDynamics.js +704 -0
  132. package/dist/physics/boatDynamics.d.ts +116 -0
  133. package/dist/physics/boatDynamics.js +160 -0
  134. package/dist/physics/drivableVehicle.d.ts +22 -8
  135. package/dist/physics/drivableVehicle.js +4 -2
  136. package/dist/physics/flightDynamics.d.ts +29 -4
  137. package/dist/physics/flightDynamics.js +28 -5
  138. package/dist/physics/handlingProbe.d.ts +249 -0
  139. package/dist/physics/handlingProbe.js +606 -0
  140. package/dist/physics/kinematicVehicle.d.ts +37 -2
  141. package/dist/physics/kinematicVehicle.js +15 -0
  142. package/dist/physics/physicsBackend.d.ts +203 -0
  143. package/dist/physics/physicsBackend.js +49 -0
  144. package/dist/physics/physicsBackendConformance.d.ts +27 -0
  145. package/dist/physics/physicsBackendConformance.js +151 -0
  146. package/dist/physics/physicsWorldBackend.d.ts +45 -0
  147. package/dist/physics/physicsWorldBackend.js +518 -0
  148. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  149. package/dist/physics/vehicleBackendLink.js +102 -0
  150. package/dist/physics/vehicleDynamics.d.ts +374 -0
  151. package/dist/physics/vehicleDynamics.js +737 -0
  152. package/dist/physics/worldColliders.d.ts +13 -0
  153. package/dist/physics/worldColliders.js +128 -0
  154. package/dist/puzzle/cellGrid.d.ts +15 -9
  155. package/dist/puzzle/cellGrid.js +13 -9
  156. package/dist/random/rng.d.ts +12 -1
  157. package/dist/random/rng.js +20 -3
  158. package/dist/render/environment.d.ts +23 -0
  159. package/dist/render/environment.js +1 -0
  160. package/dist/render/lookPreset.d.ts +19 -6
  161. package/dist/render/lookPreset.js +56 -4
  162. package/dist/render/postProcessing.d.ts +40 -1
  163. package/dist/render/postProcessing.js +1 -1
  164. package/dist/render/sprite2d.d.ts +38 -0
  165. package/dist/render/sprite2d.js +40 -0
  166. package/dist/runtime/adapter.d.ts +20 -1
  167. package/dist/runtime/adapter.js +8 -1
  168. package/dist/runtime/cameraDirector.d.ts +17 -3
  169. package/dist/runtime/cameraDirector.js +25 -0
  170. package/dist/runtime/commandInput.d.ts +52 -0
  171. package/dist/runtime/commandInput.js +71 -0
  172. package/dist/runtime/commandRunner.d.ts +39 -7
  173. package/dist/runtime/commandRunner.js +35 -5
  174. package/dist/runtime/context/combat.d.ts +1 -0
  175. package/dist/runtime/context/combat.js +2 -1
  176. package/dist/runtime/context/deathLoot.d.ts +2 -0
  177. package/dist/runtime/context/deathLoot.js +1 -0
  178. package/dist/runtime/gameContext.js +18 -1
  179. package/dist/runtime/gameContextTypes.d.ts +33 -7
  180. package/dist/runtime/gameRuntime.d.ts +23 -5
  181. package/dist/runtime/gameRuntime.js +21 -4
  182. package/dist/runtime/headlessRunner.d.ts +10 -0
  183. package/dist/runtime/headlessRunner.js +26 -8
  184. package/dist/runtime/hostPolicy.d.ts +3 -2
  185. package/dist/runtime/hostPolicy.js +8 -10
  186. package/dist/runtime/hostedGameRunner.js +34 -6
  187. package/dist/runtime/hostedWorldSession.d.ts +12 -16
  188. package/dist/runtime/hostedWorldSession.js +29 -6
  189. package/dist/runtime/hostedWorldStore.d.ts +20 -0
  190. package/dist/runtime/hostedWorldStore.js +1 -0
  191. package/dist/runtime/inputRecorder.d.ts +30 -0
  192. package/dist/runtime/inputRecorder.js +59 -0
  193. package/dist/runtime/inputSnapshot.d.ts +14 -0
  194. package/dist/runtime/inputSnapshot.js +11 -0
  195. package/dist/runtime/localPlayers.d.ts +73 -0
  196. package/dist/runtime/localPlayers.js +119 -0
  197. package/dist/runtime/poseInterpolation.d.ts +36 -0
  198. package/dist/runtime/poseInterpolation.js +91 -0
  199. package/dist/runtime/prediction.d.ts +45 -0
  200. package/dist/runtime/prediction.js +62 -0
  201. package/dist/runtime/simContext.d.ts +45 -0
  202. package/dist/runtime/simContext.js +65 -0
  203. package/dist/runtime/simLoop.d.ts +52 -0
  204. package/dist/runtime/simLoop.js +72 -0
  205. package/dist/runtime/simSnapshot.d.ts +32 -0
  206. package/dist/runtime/simSnapshot.js +60 -0
  207. package/dist/runtime/snapshot.d.ts +8 -0
  208. package/dist/runtime/snapshotBuffer.d.ts +33 -0
  209. package/dist/runtime/snapshotBuffer.js +58 -0
  210. package/dist/runtime/transport.d.ts +41 -2
  211. package/dist/runtime/transport.js +7 -1
  212. package/dist/runtime/worldChunks.d.ts +2 -0
  213. package/dist/runtime/worldChunks.js +14 -0
  214. package/dist/runtime/worldProjection.d.ts +9 -0
  215. package/dist/runtime/worldProjection.js +17 -1
  216. package/dist/runtime/worldSnapshot.d.ts +6 -0
  217. package/dist/runtime/worldSnapshot.js +3 -0
  218. package/dist/scene/behaviorRuntime.d.ts +29 -1
  219. package/dist/scene/behaviorRuntime.js +71 -4
  220. package/dist/scene/behaviors.d.ts +12 -1
  221. package/dist/scene/builtinSceneKinds.js +10 -0
  222. package/dist/scene/sceneKinds.d.ts +9 -0
  223. package/dist/scene/vehicleSeat.d.ts +5 -1
  224. package/dist/scene/vehicleSeat.js +5 -1
  225. package/dist/sensor/concealment.d.ts +6 -0
  226. package/dist/sensor/concealment.js +8 -0
  227. package/dist/sensor/freezeMonitor.d.ts +6 -0
  228. package/dist/sensor/freezeMonitor.js +8 -0
  229. package/dist/sensor/perception.d.ts +70 -0
  230. package/dist/sensor/perception.js +189 -0
  231. package/dist/sensor/recordingBuffer.d.ts +3 -0
  232. package/dist/sensor/recordingBuffer.js +8 -0
  233. package/dist/session/roundState.d.ts +4 -0
  234. package/dist/session/roundState.js +11 -0
  235. package/dist/settings/frameRateLimit.d.ts +16 -0
  236. package/dist/settings/frameRateLimit.js +37 -0
  237. package/dist/settings/graphicsProfile.d.ts +21 -0
  238. package/dist/settings/graphicsProfile.js +48 -0
  239. package/dist/settings/graphicsSettings.d.ts +40 -0
  240. package/dist/settings/graphicsSettings.js +81 -0
  241. package/dist/settings/settingsModel.d.ts +9 -1
  242. package/dist/settings/settingsModel.js +10 -0
  243. package/dist/stats/eventMeter.d.ts +1 -1
  244. package/dist/stats/eventMeter.js +1 -1
  245. package/dist/time/accrueSince.d.ts +10 -0
  246. package/dist/time/accrueSince.js +9 -0
  247. package/dist/time/rateWindow.d.ts +16 -0
  248. package/dist/time/rateWindow.js +14 -0
  249. package/dist/time/serverTick.d.ts +21 -3
  250. package/dist/time/serverTick.js +14 -13
  251. package/dist/vfx/feedbackMixer.d.ts +61 -0
  252. package/dist/vfx/feedbackMixer.js +106 -0
  253. package/dist/vfx/particleDirector.d.ts +68 -0
  254. package/dist/vfx/particleDirector.js +81 -0
  255. package/dist/visibility/simulationCulling.d.ts +8 -1
  256. package/dist/visibility/simulationCulling.js +7 -0
  257. package/dist/visibility/spatialIndex.d.ts +21 -0
  258. package/dist/visibility/spatialIndex.js +16 -0
  259. package/dist/world/authoredSolids.d.ts +46 -0
  260. package/dist/world/authoredSolids.js +75 -0
  261. package/dist/world/buildingGenerator.js +2 -2
  262. package/dist/world/buildingKit.d.ts +3 -1
  263. package/dist/world/buildings.d.ts +46 -2
  264. package/dist/world/buildings.js +53 -3
  265. package/dist/world/cityContent.js +103 -25
  266. package/dist/world/cityKind.d.ts +139 -0
  267. package/dist/world/cityKind.js +367 -16
  268. package/dist/world/features.d.ts +56 -3
  269. package/dist/world/features.js +13 -2
  270. package/dist/world/footprintGrid.d.ts +7 -0
  271. package/dist/world/footprintGrid.js +16 -0
  272. package/dist/world/interiors.d.ts +1 -0
  273. package/dist/world/interiors.js +5 -3
  274. package/dist/world/lod.d.ts +6 -0
  275. package/dist/world/lod.js +8 -0
  276. package/dist/world/placement.d.ts +7 -0
  277. package/dist/world/placement.js +2 -0
  278. package/dist/world/placementController.d.ts +1 -1
  279. package/dist/world/placementController.js +7 -0
  280. package/dist/world/raceCircuit.d.ts +168 -0
  281. package/dist/world/raceCircuit.js +797 -0
  282. package/dist/world/terraform.d.ts +8 -0
  283. package/dist/world/terraform.js +7 -0
  284. package/dist/world/terrain.d.ts +5 -1
  285. package/dist/world/terrain.js +5 -0
  286. package/dist/world/territory.d.ts +97 -0
  287. package/dist/world/territory.js +182 -0
  288. package/dist/world/walls.d.ts +7 -0
  289. package/dist/world/walls.js +7 -0
  290. package/dist/world/worldSolids.d.ts +84 -0
  291. package/dist/world/worldSolids.js +240 -0
  292. package/dist/world.d.ts +5 -3
  293. package/dist/world.js +4 -2
  294. package/package.json +1 -1
  295. package/skills/ce-handoff/SKILL.md +2 -2
  296. package/skills/game-design/SKILL.md +11 -1
  297. package/skills/jgengine/SKILL.md +9 -5
  298. package/skills/jgengine/api.md +205 -101
  299. package/skills/jgengine/capabilities.md +56 -0
  300. package/skills/jgengine/recipes/minimal-game.md +3 -0
  301. package/skills/jgengine-combat/SKILL.md +5 -0
  302. package/skills/jgengine-combat/api.md +138 -103
  303. package/skills/jgengine-combat/capabilities.md +29 -1
  304. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  305. package/skills/jgengine-gameplay/SKILL.md +10 -0
  306. package/skills/jgengine-gameplay/api.md +501 -453
  307. package/skills/jgengine-gameplay/capabilities.md +36 -0
  308. package/skills/jgengine-gameplay/reference-systems.md +4 -0
  309. package/skills/jgengine-world/SKILL.md +21 -3
  310. package/skills/jgengine-world/api.md +1172 -625
  311. package/skills/jgengine-world/capabilities.md +229 -3
  312. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  313. package/skills/jgengine-world/recipes/controls.md +44 -0
  314. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  315. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  316. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  317. package/skills/jgengine-world/reference.md +31 -2
  318. package/skills/jgengine-world/visibility.md +112 -0
@@ -0,0 +1,13 @@
1
+ import type { GameContext } from "../runtime/gameContextTypes.js";
2
+ import type { PhysicsBackend } from "./physicsBackend.js";
3
+ /** Handle for synchronizing authored static world collision with a physics backend. */
4
+ export interface WorldColliderSync {
5
+ /** Rebuild static terrain and object bodies from the current context. */
6
+ sync(): void;
7
+ /** Remove all bodies owned by this synchronizer and stop reacting to scene changes. */
8
+ dispose(): void;
9
+ }
10
+ /** Mirrors static scene-object physical colliders, `ctx.world.solids` and the context's ground field into a physics backend.
11
+ * @capability world-physics-colliders Synchronize terrain and authored static colliders with a physics backend.
12
+ */
13
+ export declare function syncWorldColliders(backend: PhysicsBackend, ctx: GameContext): WorldColliderSync;
@@ -0,0 +1,128 @@
1
+ import { colliderWorldCenter, resolveColliders } from "../scene/colliders.js";
2
+ const TERRAIN_SEGMENTS = 32;
3
+ function bodyShape(shape) {
4
+ if (shape.kind === "sphere")
5
+ return { kind: "sphere", radius: shape.radius };
6
+ if (shape.kind === "aabb")
7
+ return { kind: "box", halfExtents: shape.halfExtents };
8
+ return { kind: "trimesh", vertices: shape.mesh.positions, indices: shape.mesh.indices };
9
+ }
10
+ function bodyPosition(shape, position, rotationY) {
11
+ return colliderWorldCenter({ name: "body", purpose: "physical", shape, damageEligible: false, blocks: true }, position, rotationY);
12
+ }
13
+ /** Mirrors static scene-object physical colliders, `ctx.world.solids` and the context's ground field into a physics backend.
14
+ * @capability world-physics-colliders Synchronize terrain and authored static colliders with a physics backend.
15
+ */
16
+ export function syncWorldColliders(backend, ctx) {
17
+ const objectBodies = new Map();
18
+ const solidBodies = [];
19
+ let solidsVersion = -1;
20
+ let terrainBody = null;
21
+ let disposed = false;
22
+ function rebuildTerrain() {
23
+ if (terrainBody !== null)
24
+ backend.removeBody(terrainBody);
25
+ terrainBody = null;
26
+ const bounds = ctx.world.ground.bounds;
27
+ if (bounds === undefined)
28
+ return;
29
+ const columns = TERRAIN_SEGMENTS + 1;
30
+ const rows = TERRAIN_SEGMENTS + 1;
31
+ const heights = new Float32Array(columns * rows);
32
+ for (let row = 0; row < rows; row += 1) {
33
+ const z = -bounds.d / 2 + (row / TERRAIN_SEGMENTS) * bounds.d;
34
+ for (let column = 0; column < columns; column += 1) {
35
+ const x = -bounds.w / 2 + (column / TERRAIN_SEGMENTS) * bounds.w;
36
+ heights[row * columns + column] = ctx.world.ground.sampleHeight(x, z);
37
+ }
38
+ }
39
+ terrainBody = backend.addBody({
40
+ shape: { kind: "heightfield", rows, columns, heights, scale: [bounds.w / TERRAIN_SEGMENTS, 1, bounds.d / TERRAIN_SEGMENTS] },
41
+ position: [0, 0, 0],
42
+ kind: "static",
43
+ userData: { kind: "terrain" },
44
+ });
45
+ }
46
+ function syncObject(objectId) {
47
+ const old = objectBodies.get(objectId);
48
+ if (old !== undefined) {
49
+ backend.removeBody(old);
50
+ objectBodies.delete(objectId);
51
+ }
52
+ const object = ctx.scene.object.get(objectId);
53
+ if (object === null)
54
+ return;
55
+ const body = resolveColliders(ctx.scene.object.collidersOf(objectId)).find((collider) => collider.purpose === "physical");
56
+ if (body === undefined)
57
+ return;
58
+ const handle = backend.addBody({
59
+ shape: bodyShape(body.shape),
60
+ position: bodyPosition(body.shape, object.position, object.rotationY),
61
+ rotation: [0, Math.sin(object.rotationY / 2), 0, Math.cos(object.rotationY / 2)],
62
+ kind: "static",
63
+ userData: { kind: "object", instanceId: object.instanceId, catalogId: object.catalogId },
64
+ });
65
+ objectBodies.set(objectId, handle);
66
+ }
67
+ function syncSolids() {
68
+ const solids = ctx.world.solids;
69
+ if (solids.version() === solidsVersion)
70
+ return;
71
+ solidsVersion = solids.version();
72
+ for (const handle of solidBodies)
73
+ backend.removeBody(handle);
74
+ solidBodies.length = 0;
75
+ for (const layer of solids.layers()) {
76
+ for (const solid of solids.layer(layer)) {
77
+ const yaw = solid.rotationY ?? 0;
78
+ solidBodies.push(backend.addBody({
79
+ shape: { kind: "box", halfExtents: [solid.halfExtents[0], solid.halfExtents[1], solid.halfExtents[2]] },
80
+ position: [solid.center[0], solid.center[1], solid.center[2]],
81
+ rotation: [0, Math.sin(yaw / 2), 0, Math.cos(yaw / 2)],
82
+ kind: "static",
83
+ userData: { kind: "solid", layer },
84
+ }));
85
+ }
86
+ }
87
+ }
88
+ function sync() {
89
+ if (disposed)
90
+ return;
91
+ rebuildTerrain();
92
+ syncSolids();
93
+ const ids = new Set(ctx.scene.object.ids());
94
+ for (const [objectId, handle] of objectBodies) {
95
+ if (!ids.has(objectId)) {
96
+ backend.removeBody(handle);
97
+ objectBodies.delete(objectId);
98
+ }
99
+ }
100
+ for (const objectId of ids)
101
+ syncObject(objectId);
102
+ }
103
+ const unsubscribeObjects = ctx.scene.object.subscribe(sync);
104
+ const unsubscribeSolids = ctx.world.solids.subscribe(() => {
105
+ if (!disposed)
106
+ syncSolids();
107
+ });
108
+ sync();
109
+ return {
110
+ sync,
111
+ dispose() {
112
+ if (disposed)
113
+ return;
114
+ disposed = true;
115
+ unsubscribeObjects();
116
+ unsubscribeSolids();
117
+ for (const handle of solidBodies)
118
+ backend.removeBody(handle);
119
+ solidBodies.length = 0;
120
+ if (terrainBody !== null)
121
+ backend.removeBody(terrainBody);
122
+ terrainBody = null;
123
+ for (const handle of objectBodies.values())
124
+ backend.removeBody(handle);
125
+ objectBodies.clear();
126
+ },
127
+ };
128
+ }
@@ -1,8 +1,10 @@
1
+ /** A fixed-size 2D board of cells, row-major from the top-left; `null` is an empty cell. Plain data, so it serializes and restores as-is. */
1
2
  export interface CellGrid<T> {
2
3
  readonly width: number;
3
4
  readonly height: number;
4
5
  readonly cells: readonly (T | null)[];
5
6
  }
7
+ /** A straight line of matching cells found by {@link findRuns}. */
6
8
  export interface CellRun<T> {
7
9
  readonly value: T;
8
10
  readonly cells: readonly {
@@ -11,25 +13,29 @@ export interface CellRun<T> {
11
13
  }[];
12
14
  readonly direction: "row" | "column";
13
15
  }
14
- /** @internal */
16
+ /**
17
+ * An empty `width` × `height` board. Every helper here returns a new grid, so a store slot can hold it
18
+ * and undo, replay, and save/restore work without copying.
19
+ * @capability cell-grid immutable 2D board of cells with row clears, gravity collapse, and match runs
20
+ */
15
21
  export declare function createCellGrid<T>(width: number, height: number): CellGrid<T>;
16
- /** @internal */
22
+ /** Whether `(x, y)` lies on the board. */
17
23
  export declare function inGridBounds<T>(grid: CellGrid<T>, x: number, y: number): boolean;
18
- /** @internal */
24
+ /** The value at `(x, y)`, or `null` when empty or off the board. */
19
25
  export declare function cellAt<T>(grid: CellGrid<T>, x: number, y: number): T | null;
20
- /** @internal */
26
+ /** A copy with `(x, y)` set to `value`; off-board writes return the grid unchanged. */
21
27
  export declare function withCell<T>(grid: CellGrid<T>, x: number, y: number, value: T | null): CellGrid<T>;
22
- /** @internal */
28
+ /** A copy with every in-bounds entry written; one allocation for a whole piece or stamp. */
23
29
  export declare function withCells<T>(grid: CellGrid<T>, entries: readonly {
24
30
  readonly x: number;
25
31
  readonly y: number;
26
32
  readonly value: T | null;
27
33
  }[]): CellGrid<T>;
28
- /** @internal */
34
+ /** Indices of rows with no empty cell, top to bottom. */
29
35
  export declare function fullRows<T>(grid: CellGrid<T>): number[];
30
- /** @internal */
36
+ /** A copy with `rows` removed and the rows above shifted down, refilling empty rows at the top. */
31
37
  export declare function clearRows<T>(grid: CellGrid<T>, rows: readonly number[]): CellGrid<T>;
32
- /** @internal */
38
+ /** A copy where every column's cells fall to the bottom, keeping their order. */
33
39
  export declare function collapseColumns<T>(grid: CellGrid<T>): CellGrid<T>;
34
- /** @internal */
40
+ /** Horizontal and vertical runs of at least `minLength` cells whose values `matches` (default `===`). */
35
41
  export declare function findRuns<T>(grid: CellGrid<T>, minLength: number, matches?: (a: T, b: T) => boolean): CellRun<T>[];
@@ -1,18 +1,22 @@
1
- /** @internal */
1
+ /**
2
+ * An empty `width` × `height` board. Every helper here returns a new grid, so a store slot can hold it
3
+ * and undo, replay, and save/restore work without copying.
4
+ * @capability cell-grid immutable 2D board of cells with row clears, gravity collapse, and match runs
5
+ */
2
6
  export function createCellGrid(width, height) {
3
7
  return { width, height, cells: new Array(width * height).fill(null) };
4
8
  }
5
- /** @internal */
9
+ /** Whether `(x, y)` lies on the board. */
6
10
  export function inGridBounds(grid, x, y) {
7
11
  return x >= 0 && x < grid.width && y >= 0 && y < grid.height;
8
12
  }
9
- /** @internal */
13
+ /** The value at `(x, y)`, or `null` when empty or off the board. */
10
14
  export function cellAt(grid, x, y) {
11
15
  if (!inGridBounds(grid, x, y))
12
16
  return null;
13
17
  return grid.cells[y * grid.width + x] ?? null;
14
18
  }
15
- /** @internal */
19
+ /** A copy with `(x, y)` set to `value`; off-board writes return the grid unchanged. */
16
20
  export function withCell(grid, x, y, value) {
17
21
  if (!inGridBounds(grid, x, y))
18
22
  return grid;
@@ -20,7 +24,7 @@ export function withCell(grid, x, y, value) {
20
24
  cells[y * grid.width + x] = value;
21
25
  return { width: grid.width, height: grid.height, cells };
22
26
  }
23
- /** @internal */
27
+ /** A copy with every in-bounds entry written; one allocation for a whole piece or stamp. */
24
28
  export function withCells(grid, entries) {
25
29
  const cells = grid.cells.slice();
26
30
  for (const entry of entries) {
@@ -29,7 +33,7 @@ export function withCells(grid, entries) {
29
33
  }
30
34
  return { width: grid.width, height: grid.height, cells };
31
35
  }
32
- /** @internal */
36
+ /** Indices of rows with no empty cell, top to bottom. */
33
37
  export function fullRows(grid) {
34
38
  const rows = [];
35
39
  for (let y = 0; y < grid.height; y += 1) {
@@ -45,7 +49,7 @@ export function fullRows(grid) {
45
49
  }
46
50
  return rows;
47
51
  }
48
- /** @internal */
52
+ /** A copy with `rows` removed and the rows above shifted down, refilling empty rows at the top. */
49
53
  export function clearRows(grid, rows) {
50
54
  const removed = new Set(rows);
51
55
  const kept = [];
@@ -62,7 +66,7 @@ export function clearRows(grid, rows) {
62
66
  cells.push(...row);
63
67
  return { width: grid.width, height: grid.height, cells };
64
68
  }
65
- /** @internal */
69
+ /** A copy where every column's cells fall to the bottom, keeping their order. */
66
70
  export function collapseColumns(grid) {
67
71
  const cells = grid.cells.slice();
68
72
  for (let x = 0; x < grid.width; x += 1) {
@@ -96,7 +100,7 @@ function scanRuns(length, cellAtIndex, minLength, matches) {
96
100
  }
97
101
  return runs;
98
102
  }
99
- /** @internal */
103
+ /** Horizontal and vertical runs of at least `minLength` cells whose values `matches` (default `===`). */
100
104
  export function findRuns(grid, minLength, matches = (a, b) => a === b) {
101
105
  const runs = [];
102
106
  for (let y = 0; y < grid.height; y += 1) {
@@ -20,10 +20,21 @@ export declare function randomSeedFrom(seed: number): RandomSeed;
20
20
  * instead of closing over a generator.
21
21
  */
22
22
  export declare function stepRandomSeed(seed: RandomSeed): readonly [value: number, next: RandomSeed];
23
+ /** A {@link seededRng} stream: callable like any `() => number`, with its cursor readable and restorable for snapshots and replay. */
24
+ export interface SeededRng {
25
+ (): number;
26
+ /** The current cursor; feed it back to {@link restore} (or a fresh stream's `restore`) to resume the exact sequence. */
27
+ state(): number;
28
+ restore(cursor: number): void;
29
+ }
23
30
  /** Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
24
31
  *
25
32
  * @capability seeded-random injected deterministic randomness for game logic — never `Math.random` in a simulation
26
33
  */
27
- export declare function seededRng(seed: string | number): () => number;
34
+ export declare function seededRng(seed: string | number): SeededRng;
35
+ /** The cursor of a seeded stream, or `null` for a generator that carries no state (an injected closure, `Math.random`). */
36
+ export declare function rngStateOf(rng: () => number): number | null;
37
+ /** Restore a seeded stream's cursor; a no-op for generators that carry no state. */
38
+ export declare function restoreRng(rng: () => number, cursor: number): void;
28
39
  /** Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name. */
29
40
  export declare function seededStreams(seed: string | number): (stream: string) => () => number;
@@ -35,11 +35,28 @@ export function stepRandomSeed(seed) {
35
35
  */
36
36
  export function seededRng(seed) {
37
37
  let cursor = randomSeedFrom(hashSeed(seed));
38
- return () => {
39
- const [value, next] = stepRandomSeed(cursor);
40
- cursor = next;
38
+ const next = () => {
39
+ const [value, following] = stepRandomSeed(cursor);
40
+ cursor = following;
41
41
  return value;
42
42
  };
43
+ const rng = next;
44
+ rng.state = () => cursor;
45
+ rng.restore = (value) => {
46
+ cursor = randomSeedFrom(value);
47
+ };
48
+ return rng;
49
+ }
50
+ /** The cursor of a seeded stream, or `null` for a generator that carries no state (an injected closure, `Math.random`). */
51
+ export function rngStateOf(rng) {
52
+ const candidate = rng;
53
+ return typeof candidate.state === "function" ? candidate.state() : null;
54
+ }
55
+ /** Restore a seeded stream's cursor; a no-op for generators that carry no state. */
56
+ export function restoreRng(rng, cursor) {
57
+ const candidate = rng;
58
+ if (typeof candidate.restore === "function")
59
+ candidate.restore(cursor);
43
60
  }
44
61
  /** Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name. */
45
62
  export function seededStreams(seed) {
@@ -0,0 +1,23 @@
1
+ /** A declarative source for image-based scene lighting. */
2
+ export type EnvironmentSource =
3
+ /** Procedural two-color sky probe, preserving the engine default. */
4
+ {
5
+ kind: "gradient";
6
+ sky: string;
7
+ ground: string;
8
+ sun?: string;
9
+ intensity?: number;
10
+ }
11
+ /** Equirectangular HDR or EXR environment map loaded from `url`. */
12
+ | {
13
+ kind: "hdri";
14
+ url: string;
15
+ rotation?: number;
16
+ intensity?: number;
17
+ }
18
+ /** Six faces of a cube environment map, in three.js cube-loader order. */
19
+ | {
20
+ kind: "cube";
21
+ urls: [string, string, string, string, string, string];
22
+ intensity?: number;
23
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -2,16 +2,19 @@ import type { BackdropConfig, LightingConfig } from "../game/playableGame.js";
2
2
  import type { SkyEnvironmentConfig } from "../world/features.js";
3
3
  import type { PostProcessingConfig } from "./postProcessing.js";
4
4
  /**
5
- * Named default-look preset composing the existing lighting/sky/fog/post knobs into one field.
6
- * `"cinematic"` (the default when unset) draws a scene lit like a shipped game — a real day sky
5
+ * Named look preset composing the existing lighting/sky/fog/post knobs into one field.
6
+ * `"neutral"` (the default when unset) keeps presentation unstyled; `"cinematic"` draws a scene lit like a shipped game — a real day sky
7
7
  * with a view-following shadow-casting sun + hemisphere fill, a network-free image-based-lighting
8
8
  * environment so PBR surfaces catch soft reflections, and a tuned tone-map/bloom/AO/vignette post
9
- * stack. `"flat"` opts out of the sky/IBL/post rig to the bare ambient+directional default (pre-#773).
9
+ * stack. `"comic"` adds ink outlines and cel bands over a saturated grade (Borderlands-style);
10
+ * `"retro"` pixelates and posterizes the frame. `"flat"` opts out of the sky/IBL/post rig to the bare ambient+directional default (pre-#773).
10
11
  * The upgraded default primitive materials — tuned roughness/metalness plus subtle procedural surface
11
12
  * detail so un-modeled boxes/capsules stop reading as flat plastic — apply under both presets.
12
13
  * @capability default-look one field that lights a scene like a shipped game (opt out with "flat")
13
14
  */
14
- export type LookPreset = "cinematic" | "flat";
15
+ export type LookPreset = "neutral" | "photoreal" | "toon" | "comic" | "retro" | "cinematic" | "flat";
16
+ /** Every look preset name, for pickers and validating a preset read from a URL or file. */
17
+ export declare const LOOK_PRESETS: readonly LookPreset[];
15
18
  /** Explicit graphics knobs a game may set; the preset only fills the ones left undefined. */
16
19
  export interface GameLookInput {
17
20
  look?: LookPreset;
@@ -31,9 +34,19 @@ export interface ResolvedGameLook {
31
34
  export declare const CINEMATIC_SKY: SkyEnvironmentConfig;
32
35
  /** Tuned tone-map + bloom + gentle SSAO + vignette/grade stack — the shipped-game post look. */
33
36
  export declare const CINEMATIC_POST_PROCESSING: PostProcessingConfig;
37
+ /** Unstyled baseline: a stable linear presentation with no genre-specific grading. */
38
+ export declare const NEUTRAL_POST_PROCESSING: PostProcessingConfig;
39
+ /** High-contrast physically based presentation for authored HDR environments. */
40
+ export declare const PHOTOREAL_POST_PROCESSING: PostProcessingConfig;
41
+ /** Clean graphic presentation; toon materials remain game-owned while this sets the grade. */
42
+ export declare const TOON_POST_PROCESSING: PostProcessingConfig;
43
+ /** Ink outlines, four cel bands and a punchy grade — a hand-inked comic look on any model. */
44
+ export declare const COMIC_POST_PROCESSING: PostProcessingConfig;
45
+ /** Chunky pixels and posterized colour with no smoothing — a low-resolution retro look. */
46
+ export declare const RETRO_POST_PROCESSING: PostProcessingConfig;
34
47
  /**
35
- * Expand a game's `look` into concrete lighting/backdrop/post. The default is `"cinematic"`, so a
36
- * scene reads lit-like-a-game out of the box; `"flat"` passes the explicit knobs through untouched.
48
+ * Expand a game's `look` into concrete lighting/backdrop/post. The default is `"neutral"`;
49
+ * `"flat"` passes the explicit knobs through untouched.
37
50
  * Anything the game authored wins — the preset only fills unset knobs, and it never adds a sky when
38
51
  * the world already owns one (so the sky's tuned sun/hemisphere serve as the lighting rig).
39
52
  * @capability resolve-game-look expand a look preset into concrete lighting/backdrop/post knobs
@@ -1,3 +1,5 @@
1
+ /** Every look preset name, for pickers and validating a preset read from a URL or file. */
2
+ export const LOOK_PRESETS = ["neutral", "photoreal", "cinematic", "toon", "comic", "retro", "flat"];
1
3
  /** A static day sky: a real dome plus the shell's view-following shadow-casting sun and hemisphere fill. */
2
4
  export const CINEMATIC_SKY = { preset: "day" };
3
5
  /** Tuned tone-map + bloom + gentle SSAO + vignette/grade stack — the shipped-game post look. */
@@ -7,23 +9,73 @@ export const CINEMATIC_POST_PROCESSING = {
7
9
  ao: { intensity: 1.4 },
8
10
  grade: {},
9
11
  };
12
+ /** Unstyled baseline: a stable linear presentation with no genre-specific grading. */
13
+ export const NEUTRAL_POST_PROCESSING = {
14
+ toneMapping: "agx",
15
+ bloom: false,
16
+ ao: false,
17
+ grade: false,
18
+ };
19
+ /** High-contrast physically based presentation for authored HDR environments. */
20
+ export const PHOTOREAL_POST_PROCESSING = {
21
+ toneMapping: "aces",
22
+ bloom: { strength: 0.28, radius: 0.58, threshold: 0.9 },
23
+ ao: { intensity: 1.25, radius: 1.6 },
24
+ grade: { saturation: 1.04, vignette: 0.08, grain: 0.004 },
25
+ };
26
+ /** Clean graphic presentation; toon materials remain game-owned while this sets the grade. */
27
+ export const TOON_POST_PROCESSING = {
28
+ toneMapping: "linear",
29
+ bloom: { strength: 0.08, radius: 0.35, threshold: 1.1 },
30
+ ao: { intensity: 0.8, radius: 1.2 },
31
+ grade: { saturation: 1.18, gamma: 1, vignette: 0, grain: 0 },
32
+ };
33
+ /** Ink outlines, four cel bands and a punchy grade — a hand-inked comic look on any model. */
34
+ export const COMIC_POST_PROCESSING = {
35
+ toneMapping: "aces",
36
+ exposure: 1.05,
37
+ aa: "msaa",
38
+ bloom: { strength: 0.12, radius: 0.4, threshold: 1 },
39
+ ao: { intensity: 1, radius: 1.2 },
40
+ stylize: { outline: { thickness: 1.5 }, bands: 4 },
41
+ grade: { saturation: 1.3, gamma: 0.94, vignette: 0.15, grain: 0 },
42
+ };
43
+ /** Chunky pixels and posterized colour with no smoothing — a low-resolution retro look. */
44
+ export const RETRO_POST_PROCESSING = {
45
+ toneMapping: "aces",
46
+ aa: false,
47
+ bloom: false,
48
+ ao: false,
49
+ stylize: { pixelSize: 3, bands: 6 },
50
+ grade: { saturation: 1.15, gamma: 1, vignette: 0, grain: 0 },
51
+ };
52
+ const PRESET_POST = {
53
+ photoreal: PHOTOREAL_POST_PROCESSING,
54
+ toon: TOON_POST_PROCESSING,
55
+ comic: COMIC_POST_PROCESSING,
56
+ retro: RETRO_POST_PROCESSING,
57
+ cinematic: CINEMATIC_POST_PROCESSING,
58
+ };
10
59
  /**
11
- * Expand a game's `look` into concrete lighting/backdrop/post. The default is `"cinematic"`, so a
12
- * scene reads lit-like-a-game out of the box; `"flat"` passes the explicit knobs through untouched.
60
+ * Expand a game's `look` into concrete lighting/backdrop/post. The default is `"neutral"`;
61
+ * `"flat"` passes the explicit knobs through untouched.
13
62
  * Anything the game authored wins — the preset only fills unset knobs, and it never adds a sky when
14
63
  * the world already owns one (so the sky's tuned sun/hemisphere serve as the lighting rig).
15
64
  * @capability resolve-game-look expand a look preset into concrete lighting/backdrop/post knobs
16
65
  */
17
66
  export function resolveGameLook(input) {
18
- const look = input.look ?? "cinematic";
67
+ const look = input.look ?? "neutral";
19
68
  if (look === "flat") {
20
69
  return { lighting: input.lighting, backdrop: input.backdrop, postProcessing: input.postProcessing };
21
70
  }
71
+ if (look === "neutral") {
72
+ return { lighting: input.lighting, backdrop: input.backdrop, postProcessing: input.postProcessing ?? NEUTRAL_POST_PROCESSING };
73
+ }
22
74
  const sky = input.backdrop?.sky ?? (input.hasWorldSky === true ? undefined : CINEMATIC_SKY);
23
75
  const backdrop = sky === input.backdrop?.sky ? input.backdrop : { ...input.backdrop, sky };
24
76
  return {
25
77
  lighting: input.lighting,
26
78
  backdrop,
27
- postProcessing: input.postProcessing ?? CINEMATIC_POST_PROCESSING,
79
+ postProcessing: input.postProcessing ?? PRESET_POST[look],
28
80
  };
29
81
  }
@@ -6,7 +6,11 @@ export interface BloomConfig {
6
6
  strength?: number;
7
7
  /** Glow spread radius, 0..1. Default 0.55. */
8
8
  radius?: number;
9
- /** Luminance above which a pixel blooms, 0..1. Default 0.85. */
9
+ /**
10
+ * Luminance above which a pixel blooms. Default 0.85. Measured on raw HDR
11
+ * before the tone-map/exposure stage, so lowering `exposure` does not change
12
+ * which pixels bloom — push past 1.0 to select only emissives.
13
+ */
10
14
  threshold?: number;
11
15
  }
12
16
  /** Ground-truth ambient occlusion stage — darkens contact creases and cavities for depth. */
@@ -22,6 +26,11 @@ export interface AoConfig {
22
26
  }
23
27
  /** Final colour-grade stage: lift/gain/gamma, saturation, vignette, film grain — applied in display space after tone mapping. */
24
28
  export interface GradeConfig {
29
+ /** Optional 2D strip LUT URL. The strip contains size square tiles, one tile per blue slice. */
30
+ lut?: {
31
+ url: string;
32
+ size?: number;
33
+ };
25
34
  /** Additive shadow tint (RGB, ~0..0.1). Default cool [0.012, 0.010, 0.018]. */
26
35
  lift?: readonly [number, number, number];
27
36
  /** Multiplicative highlight tint (RGB, ~1). Default warm [1.05, 1.02, 0.98]. */
@@ -46,6 +55,34 @@ export interface DofConfig {
46
55
  /** Maximum blur radius, 0..1. Default 0.01. */
47
56
  maxBlur?: number;
48
57
  }
58
+ /** Ink outlines traced from depth and surface-normal edges — silhouettes plus interior creases. */
59
+ export interface OutlineConfig {
60
+ /** Line colour (CSS). Default near-black "#101014". */
61
+ color?: string;
62
+ /** Line thickness in render pixels. Default 1. */
63
+ thickness?: number;
64
+ /** Line opacity, 0..1. Default 1. */
65
+ opacity?: number;
66
+ /** Relative depth jump that counts as a silhouette edge. Lower draws more lines. Default 0.08. */
67
+ depthThreshold?: number;
68
+ /** Normal difference (1 − cos angle) that counts as a crease. Lower draws more lines. Default 0.35. */
69
+ normalThreshold?: number;
70
+ /** Distance in world units at which lines have faded out, so far scenery stays clean. Default 160. */
71
+ fadeDistance?: number;
72
+ }
73
+ /**
74
+ * Art-style stage that turns the same models and lighting into a different look without
75
+ * touching materials: ink outlines (comic / Borderlands), cel bands (cartoon), pixelation (retro).
76
+ * Runs after tone mapping, before the grade.
77
+ */
78
+ export interface StylizeConfig {
79
+ /** Ink outlines. Omitted or `false` draws none. */
80
+ outline?: OutlineConfig | false;
81
+ /** Quantize brightness into this many cel-shading bands, keeping hue. 0 or omitted is off; 3–6 reads as cartoon. */
82
+ bands?: number;
83
+ /** Snap the frame to blocks of this many render pixels for a retro look. 0, 1 or omitted is off. */
84
+ pixelSize?: number;
85
+ }
49
86
  /**
50
87
  * Edge antialiasing for the post chain. SMAA runs in linear-sRGB before the output
51
88
  * tone-map and cleans alpha-tested foliage/particles that MSAA samples alone leave crawling.
@@ -77,6 +114,8 @@ export interface PostProcessingConfig {
77
114
  /** Depth-of-field / bokeh. Heavier stage — omit or `false` to skip. Default off (omitted). */
78
115
  dof?: DofConfig | false;
79
116
  grade?: GradeConfig | false;
117
+ /** Art-style stage: outlines, cel bands, pixelation. Default off (omitted). */
118
+ stylize?: StylizeConfig | false;
80
119
  }
81
120
  /**
82
121
  * A cinematic "product shot" post preset — the full chain on (contact-AO, soft bloom, a warm film
@@ -8,7 +8,7 @@ export const STUDIO_STAGE_POST = {
8
8
  toneMapping: "aces",
9
9
  exposure: 1.05,
10
10
  bloom: { strength: 0.28, radius: 0.6, threshold: 0.8 },
11
- ao: { radius: 1.4, intensity: 2.2, distanceFalloff: 3, blend: 1 },
11
+ ao: { radius: 1.1, intensity: 1.5, distanceFalloff: 3, blend: 1 },
12
12
  grade: {
13
13
  lift: [0.008, 0.008, 0.014],
14
14
  gain: [1.05, 1.02, 0.98],
@@ -0,0 +1,38 @@
1
+ import type { SpriteAtlas } from "../assets/spriteAtlas.js";
2
+ /** Serializable playback position for a named sprite animation. */
3
+ export interface SpriteClipState {
4
+ animation: string;
5
+ frameIndex: number;
6
+ elapsed: number;
7
+ done: boolean;
8
+ }
9
+ /** Create a renderer-independent sprite animation player.
10
+ * @capability sprite-2d animate a sprite atlas clip from headless or rendered game code
11
+ */
12
+ export declare function createSpriteClipPlayer(atlas: SpriteAtlas): {
13
+ play(name: string): void;
14
+ advance(dt: number): void;
15
+ frame: () => {
16
+ x: number;
17
+ y: number;
18
+ w: number;
19
+ h: number;
20
+ pivot?: [number, number];
21
+ } | undefined;
22
+ snapshot: () => {
23
+ animation: string;
24
+ frameIndex: number;
25
+ elapsed: number;
26
+ done: boolean;
27
+ };
28
+ restore(next: SpriteClipState): void;
29
+ retune(next: {
30
+ speed: number;
31
+ }): void;
32
+ };
33
+ /** Canonical layers used by 2D presentations. */
34
+ export declare const SortingLayers: readonly ["background", "world", "actors", "effects", "foreground", "ui"];
35
+ /** Resolve a layer name and local offset into a stable render order.
36
+ * @capability sprite-2d order sprites by named presentation layers
37
+ */
38
+ export declare function sortingOrder(layers: readonly string[], layer: string, offset?: number): number;
@@ -0,0 +1,40 @@
1
+ /** Create a renderer-independent sprite animation player.
2
+ * @capability sprite-2d animate a sprite atlas clip from headless or rendered game code
3
+ */
4
+ export function createSpriteClipPlayer(atlas) {
5
+ let state = { animation: Object.keys(atlas.animations)[0] ?? "", frameIndex: 0, elapsed: 0, done: false };
6
+ let speed = 1;
7
+ const current = () => { const animation = atlas.animations[state.animation]; if (!animation)
8
+ return undefined; return atlas.frames[animation.frames[state.frameIndex]]; };
9
+ return {
10
+ play(name) { if (!atlas.animations[name])
11
+ throw new Error(`Unknown sprite animation: ${name}.`); state = { animation: name, frameIndex: 0, elapsed: 0, done: false }; },
12
+ advance(dt) { if (!Number.isFinite(dt) || dt < 0)
13
+ throw new Error("dt must be non-negative."); const animation = atlas.animations[state.animation]; if (!animation || state.done)
14
+ return; state.elapsed += dt * speed; const duration = 1 / animation.fps; while (state.elapsed >= duration && !state.done) {
15
+ state.elapsed -= duration;
16
+ if (state.frameIndex + 1 < animation.frames.length)
17
+ state.frameIndex += 1;
18
+ else if (animation.loop === false) {
19
+ state.frameIndex = animation.frames.length - 1;
20
+ state.done = true;
21
+ }
22
+ else
23
+ state.frameIndex = 0;
24
+ } },
25
+ frame: current,
26
+ snapshot: () => ({ ...state }),
27
+ restore(next) { if (!atlas.animations[next.animation])
28
+ throw new Error(`Unknown sprite animation: ${next.animation}.`); state = { ...next }; },
29
+ retune(next) { if (!Number.isFinite(next.speed) || next.speed < 0)
30
+ throw new Error("speed must be non-negative."); speed = next.speed; },
31
+ };
32
+ }
33
+ /** Canonical layers used by 2D presentations. */
34
+ export const SortingLayers = ["background", "world", "actors", "effects", "foreground", "ui"];
35
+ /** Resolve a layer name and local offset into a stable render order.
36
+ * @capability sprite-2d order sprites by named presentation layers
37
+ */
38
+ export function sortingOrder(layers, layer, offset = 0) { const index = layers.indexOf(layer); if (index < 0)
39
+ throw new Error(`Unknown sorting layer: ${layer}.`); if (!Number.isFinite(offset))
40
+ throw new Error("offset must be finite."); return index + offset; }