@jgengine/core 0.8.0 → 0.10.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 (303) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/ai/flock.d.ts +36 -0
  3. package/dist/ai/flock.js +182 -0
  4. package/dist/ai/groupAssist.d.ts +19 -0
  5. package/dist/ai/groupAssist.js +59 -0
  6. package/dist/ai/laneSelect.d.ts +21 -0
  7. package/dist/ai/laneSelect.js +37 -0
  8. package/dist/ai/mobBrain.d.ts +68 -0
  9. package/dist/ai/mobBrain.js +115 -0
  10. package/dist/ai/spawnDirector.d.ts +13 -0
  11. package/dist/ai/spawnDirector.js +41 -17
  12. package/dist/ai/spawnPoint.d.ts +5 -0
  13. package/dist/ai/spawnPoint.js +5 -0
  14. package/dist/ai/threat.d.ts +2 -0
  15. package/dist/ai/threat.js +34 -0
  16. package/dist/audio/audioFalloff.d.ts +5 -2
  17. package/dist/audio/music.d.ts +53 -0
  18. package/dist/audio/music.js +34 -0
  19. package/dist/audio/synth.d.ts +60 -0
  20. package/dist/audio/synth.js +8 -0
  21. package/dist/authoring.d.ts +14 -0
  22. package/dist/authoring.js +14 -0
  23. package/dist/cartridge/runtime.d.ts +15 -0
  24. package/dist/cartridge/runtime.js +556 -0
  25. package/dist/cartridge/spec.d.ts +188 -0
  26. package/dist/cartridge/spec.js +19 -0
  27. package/dist/cartridge/validate.d.ts +2 -0
  28. package/dist/cartridge/validate.js +78 -0
  29. package/dist/combat/abilityKit.d.ts +17 -2
  30. package/dist/combat/abilityKit.js +62 -13
  31. package/dist/combat/castRunner.d.ts +40 -0
  32. package/dist/combat/castRunner.js +56 -0
  33. package/dist/combat/comboPoints.d.ts +14 -0
  34. package/dist/combat/comboPoints.js +52 -0
  35. package/dist/combat/defensiveWindow.d.ts +1 -1
  36. package/dist/combat/defensiveWindow.js +5 -3
  37. package/dist/combat/dotField.d.ts +43 -0
  38. package/dist/combat/dotField.js +48 -0
  39. package/dist/combat/projectiles.d.ts +33 -12
  40. package/dist/combat/projectiles.js +214 -129
  41. package/dist/combat/regenShield.d.ts +35 -0
  42. package/dist/combat/regenShield.js +45 -0
  43. package/dist/combat/resistance.d.ts +16 -4
  44. package/dist/combat/resistance.js +43 -7
  45. package/dist/combat/resourcePool.d.ts +20 -0
  46. package/dist/combat/resourcePool.js +55 -0
  47. package/dist/combat/shotOrigin.d.ts +52 -0
  48. package/dist/combat/shotOrigin.js +86 -0
  49. package/dist/combat/telegraph.d.ts +25 -0
  50. package/dist/combat/telegraph.js +47 -0
  51. package/dist/crafting/recipe.js +4 -1
  52. package/dist/devtools/devtools.d.ts +72 -24
  53. package/dist/devtools/devtools.js +449 -101
  54. package/dist/devtools/transformTunables.js +3 -3
  55. package/dist/devtools/tunableSchema.d.ts +140 -0
  56. package/dist/devtools/tunableSchema.js +476 -0
  57. package/dist/editor/commands.d.ts +65 -0
  58. package/dist/editor/commands.js +193 -0
  59. package/dist/editor/document.d.ts +40 -0
  60. package/dist/editor/document.js +153 -0
  61. package/dist/editor/index.d.ts +30 -0
  62. package/dist/editor/index.js +6 -0
  63. package/dist/editor/types.d.ts +69 -0
  64. package/dist/editor/types.js +29 -0
  65. package/dist/faction/factions.d.ts +33 -0
  66. package/dist/faction/factions.js +76 -0
  67. package/dist/faction/reputation.d.ts +32 -0
  68. package/dist/faction/reputation.js +82 -0
  69. package/dist/game/chat.d.ts +2 -0
  70. package/dist/game/chat.js +9 -1
  71. package/dist/game/chatFilter.d.ts +15 -0
  72. package/dist/game/chatFilter.js +55 -0
  73. package/dist/game/connectedPlayers.d.ts +28 -0
  74. package/dist/game/connectedPlayers.js +21 -0
  75. package/dist/game/controlGate.d.ts +4 -0
  76. package/dist/game/controlGate.js +7 -0
  77. package/dist/game/defineGame.d.ts +44 -1
  78. package/dist/game/defineGame.js +1 -0
  79. package/dist/game/events.d.ts +25 -0
  80. package/dist/game/gamePhase.d.ts +20 -0
  81. package/dist/game/gamePhase.js +21 -0
  82. package/dist/game/keyValueStore.d.ts +25 -0
  83. package/dist/game/keyValueStore.js +61 -0
  84. package/dist/game/levelSequence.d.ts +24 -2
  85. package/dist/game/levelSequence.js +51 -2
  86. package/dist/game/lootTable.d.ts +14 -1
  87. package/dist/game/lootTable.js +28 -7
  88. package/dist/game/modelAnimation.d.ts +8 -0
  89. package/dist/game/modelAnimation.js +18 -0
  90. package/dist/game/playableGame.d.ts +116 -4
  91. package/dist/game/playableGame.js +1 -1
  92. package/dist/game/quest.d.ts +7 -3
  93. package/dist/game/quest.js +17 -8
  94. package/dist/game/race.d.ts +39 -1
  95. package/dist/game/race.js +96 -11
  96. package/dist/game/recordBook.d.ts +27 -0
  97. package/dist/game/recordBook.js +86 -0
  98. package/dist/game/talents.d.ts +47 -0
  99. package/dist/game/talents.js +140 -0
  100. package/dist/index.d.ts +8 -0
  101. package/dist/index.js +8 -0
  102. package/dist/input/actionBindings.d.ts +2 -0
  103. package/dist/input/actionBindings.js +1 -0
  104. package/dist/input/axisInput.d.ts +33 -3
  105. package/dist/input/axisInput.js +60 -4
  106. package/dist/input/bindingOverrides.d.ts +25 -0
  107. package/dist/input/bindingOverrides.js +95 -0
  108. package/dist/input/pointerAxis.d.ts +30 -0
  109. package/dist/input/pointerAxis.js +31 -0
  110. package/dist/input/touchScheme.d.ts +6 -0
  111. package/dist/input/touchScheme.js +45 -7
  112. package/dist/interaction/contextMenu.d.ts +1 -0
  113. package/dist/interaction/contextMenu.js +1 -0
  114. package/dist/interaction/proximityPrompt.d.ts +3 -1
  115. package/dist/interaction/proximityPrompt.js +3 -3
  116. package/dist/meta/changelog.d.ts +1 -1
  117. package/dist/meta/changelog.js +44 -1
  118. package/dist/movement/dash.d.ts +3 -0
  119. package/dist/movement/dash.js +19 -5
  120. package/dist/movement/glideModel.d.ts +44 -0
  121. package/dist/movement/glideModel.js +54 -0
  122. package/dist/movement/grappleSwing.d.ts +34 -0
  123. package/dist/movement/grappleSwing.js +56 -0
  124. package/dist/movement/leaderTrail.d.ts +29 -0
  125. package/dist/movement/leaderTrail.js +67 -0
  126. package/dist/movement/movementModel.d.ts +4 -0
  127. package/dist/movement/movementModel.js +13 -0
  128. package/dist/movement/playerMovement.d.ts +41 -0
  129. package/dist/movement/playerMovement.js +181 -0
  130. package/dist/movement/steering.d.ts +27 -0
  131. package/dist/movement/steering.js +40 -0
  132. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  133. package/dist/multiplayer/feedWriteGate.js +12 -0
  134. package/dist/multiplayer/presenceModel.d.ts +14 -2
  135. package/dist/multiplayer/presenceModel.js +15 -3
  136. package/dist/nav/corridors.d.ts +30 -0
  137. package/dist/nav/corridors.js +69 -0
  138. package/dist/nav/navGrid.js +30 -4
  139. package/dist/nav/railGraph.d.ts +66 -0
  140. package/dist/nav/railGraph.js +129 -0
  141. package/dist/nav/timetable.d.ts +41 -0
  142. package/dist/nav/timetable.js +94 -0
  143. package/dist/physics/buoyancy.d.ts +6 -0
  144. package/dist/physics/buoyancy.js +14 -0
  145. package/dist/physics/flowTube.d.ts +32 -0
  146. package/dist/physics/flowTube.js +64 -0
  147. package/dist/physics/forceVolume.d.ts +27 -0
  148. package/dist/physics/forceVolume.js +38 -0
  149. package/dist/physics/kinematicVehicle.d.ts +52 -0
  150. package/dist/physics/kinematicVehicle.js +81 -0
  151. package/dist/physics/physicsWorld.d.ts +13 -6
  152. package/dist/physics/physicsWorld.js +169 -40
  153. package/dist/physics/ragdoll.js +1 -1
  154. package/dist/physics/vehicleBody.js +2 -1
  155. package/dist/random/rng.d.ts +2 -0
  156. package/dist/random/rng.js +2 -0
  157. package/dist/random/seedLink.d.ts +5 -0
  158. package/dist/random/seedLink.js +35 -0
  159. package/dist/render/postProcessing.d.ts +56 -0
  160. package/dist/render/postProcessing.js +1 -0
  161. package/dist/runtime/adapter.d.ts +19 -0
  162. package/dist/runtime/adapter.js +11 -5
  163. package/dist/runtime/cameraDirector.d.ts +6 -1
  164. package/dist/runtime/cameraDirector.js +6 -0
  165. package/dist/runtime/commandRunner.d.ts +2 -2
  166. package/dist/runtime/commandRunner.js +2 -2
  167. package/dist/runtime/gameContext.d.ts +72 -8
  168. package/dist/runtime/gameContext.js +374 -61
  169. package/dist/runtime/gameRuntime.d.ts +5 -1
  170. package/dist/runtime/gameRuntime.js +13 -12
  171. package/dist/runtime/hostPersistence.d.ts +9 -1
  172. package/dist/runtime/hostPersistence.js +23 -8
  173. package/dist/runtime/hostedGameRunner.d.ts +49 -0
  174. package/dist/runtime/hostedGameRunner.js +62 -0
  175. package/dist/runtime/hostedWorldSession.d.ts +65 -0
  176. package/dist/runtime/hostedWorldSession.js +57 -0
  177. package/dist/runtime/inputSnapshot.d.ts +18 -0
  178. package/dist/runtime/inputSnapshot.js +7 -0
  179. package/dist/runtime/motionIntents.d.ts +16 -6
  180. package/dist/runtime/motionIntents.js +31 -2
  181. package/dist/runtime/snapshot.d.ts +1 -0
  182. package/dist/runtime/snapshot.js +1 -1
  183. package/dist/runtime/visibility.d.ts +74 -0
  184. package/dist/runtime/visibility.js +127 -0
  185. package/dist/runtime/worldChannel.d.ts +58 -0
  186. package/dist/runtime/worldChannel.js +69 -0
  187. package/dist/runtime/worldMirror.d.ts +24 -0
  188. package/dist/runtime/worldMirror.js +31 -0
  189. package/dist/runtime/worldReplication.d.ts +46 -0
  190. package/dist/runtime/worldReplication.js +185 -0
  191. package/dist/runtime/worldSnapshot.d.ts +17 -0
  192. package/dist/runtime/worldSnapshot.js +15 -0
  193. package/dist/scene/assetCatalog.d.ts +1 -0
  194. package/dist/scene/colliders.d.ts +50 -0
  195. package/dist/scene/colliders.js +101 -0
  196. package/dist/scene/entityStats.d.ts +4 -0
  197. package/dist/scene/entityStats.js +21 -0
  198. package/dist/scene/entityStore.d.ts +51 -5
  199. package/dist/scene/entityStore.js +119 -12
  200. package/dist/scene/objectQuery.d.ts +13 -2
  201. package/dist/scene/objectQuery.js +34 -2
  202. package/dist/scene/objectStore.d.ts +2 -0
  203. package/dist/scene/objectStore.js +31 -12
  204. package/dist/scene/sceneRaycast.d.ts +80 -0
  205. package/dist/scene/sceneRaycast.js +284 -0
  206. package/dist/scene/spatial.d.ts +11 -16
  207. package/dist/scene/spatial.js +99 -38
  208. package/dist/sensor/recordingBuffer.d.ts +6 -0
  209. package/dist/sensor/recordingBuffer.js +7 -0
  210. package/dist/sensor/replayLoop.d.ts +39 -0
  211. package/dist/sensor/replayLoop.js +64 -0
  212. package/dist/sensor/visionCone.d.ts +31 -0
  213. package/dist/sensor/visionCone.js +72 -0
  214. package/dist/session/extraction.d.ts +2 -2
  215. package/dist/session/extraction.js +13 -2
  216. package/dist/settings/settingsModel.d.ts +97 -0
  217. package/dist/settings/settingsModel.js +100 -0
  218. package/dist/stats/statModifiers.d.ts +5 -2
  219. package/dist/stats/statModifiers.js +32 -14
  220. package/dist/store/observableKeyedStore.d.ts +4 -0
  221. package/dist/store/observableKeyedStore.js +17 -2
  222. package/dist/survival/decayMeter.d.ts +1 -1
  223. package/dist/survival/decayMeter.js +0 -1
  224. package/dist/tactics/fallingGrid.d.ts +2 -2
  225. package/dist/tactics/fallingGrid.js +2 -2
  226. package/dist/time/beatClock.d.ts +18 -0
  227. package/dist/time/beatClock.js +24 -0
  228. package/dist/time/idleProgress.d.ts +29 -0
  229. package/dist/time/idleProgress.js +31 -0
  230. package/dist/time/serverTick.d.ts +7 -1
  231. package/dist/time/serverTick.js +27 -4
  232. package/dist/time/stateSchedule.d.ts +57 -0
  233. package/dist/time/stateSchedule.js +121 -0
  234. package/dist/turn/commit.d.ts +2 -2
  235. package/dist/turn/commit.js +2 -2
  236. package/dist/ui/gameLayout.d.ts +125 -0
  237. package/dist/ui/gameLayout.js +129 -0
  238. package/dist/ui/hudLayout.d.ts +58 -0
  239. package/dist/ui/hudLayout.js +207 -0
  240. package/dist/ui/hudScale.d.ts +44 -0
  241. package/dist/ui/hudScale.js +55 -0
  242. package/dist/ui/orientation.d.ts +39 -0
  243. package/dist/ui/orientation.js +49 -0
  244. package/dist/ui/swingTimer.d.ts +42 -0
  245. package/dist/ui/swingTimer.js +38 -0
  246. package/dist/visibility/assetStreaming.d.ts +66 -0
  247. package/dist/visibility/assetStreaming.js +201 -0
  248. package/dist/visibility/bounds.d.ts +66 -0
  249. package/dist/visibility/bounds.js +113 -0
  250. package/dist/visibility/camera.d.ts +21 -0
  251. package/dist/visibility/camera.js +3 -0
  252. package/dist/visibility/config.d.ts +21 -0
  253. package/dist/visibility/config.js +1 -0
  254. package/dist/visibility/diagnostics.d.ts +26 -0
  255. package/dist/visibility/diagnostics.js +36 -0
  256. package/dist/visibility/distance.d.ts +11 -0
  257. package/dist/visibility/distance.js +30 -0
  258. package/dist/visibility/frustum.d.ts +47 -0
  259. package/dist/visibility/frustum.js +186 -0
  260. package/dist/visibility/occlusion.d.ts +36 -0
  261. package/dist/visibility/occlusion.js +45 -0
  262. package/dist/visibility/settings.d.ts +75 -0
  263. package/dist/visibility/settings.js +49 -0
  264. package/dist/visibility/simulationCulling.d.ts +34 -0
  265. package/dist/visibility/simulationCulling.js +36 -0
  266. package/dist/visibility/spatialIndex.d.ts +45 -0
  267. package/dist/visibility/spatialIndex.js +212 -0
  268. package/dist/visibility/visibilitySystem.d.ts +90 -0
  269. package/dist/visibility/visibilitySystem.js +257 -0
  270. package/dist/world/buildings.d.ts +6 -0
  271. package/dist/world/buildings.js +152 -0
  272. package/dist/world/carve.d.ts +7 -2
  273. package/dist/world/carve.js +17 -7
  274. package/dist/world/cellStates.d.ts +45 -0
  275. package/dist/world/cellStates.js +103 -0
  276. package/dist/world/environmentSummary.d.ts +21 -4
  277. package/dist/world/environmentSummary.js +22 -3
  278. package/dist/world/features.d.ts +185 -8
  279. package/dist/world/features.js +64 -6
  280. package/dist/world/gridInstances.d.ts +7 -8
  281. package/dist/world/gridInstances.js +145 -7
  282. package/dist/world/lod.d.ts +17 -0
  283. package/dist/world/lod.js +66 -0
  284. package/dist/world/mapLayers.d.ts +60 -0
  285. package/dist/world/mapLayers.js +44 -0
  286. package/dist/world/massing.d.ts +45 -0
  287. package/dist/world/massing.js +581 -0
  288. package/dist/world/minimap.d.ts +13 -1
  289. package/dist/world/minimap.js +21 -2
  290. package/dist/world/roads.d.ts +47 -0
  291. package/dist/world/roads.js +146 -0
  292. package/dist/world/streets.d.ts +80 -0
  293. package/dist/world/streets.js +144 -0
  294. package/dist/world/terrain.d.ts +69 -3
  295. package/dist/world/terrain.js +309 -16
  296. package/dist/world/water.d.ts +7 -0
  297. package/dist/world/water.js +19 -4
  298. package/dist/world/weather.d.ts +3 -18
  299. package/dist/world/weather.js +4 -17
  300. package/dist/world/windZones.d.ts +47 -0
  301. package/dist/world/windZones.js +59 -0
  302. package/llms.txt +3013 -1726
  303. package/package.json +12 -2
@@ -5,7 +5,7 @@ import { type GameRuntimeSnapshot, type RuntimeChunkRow, type RuntimePlayerRow,
5
5
  export type ServerLoopHooks = {
6
6
  onInit?: (ctx: RuntimeInitContext) => void;
7
7
  onNewPlayer?: (ctx: RuntimeLoopContext) => void;
8
- onTick?: (ctx: RuntimeLoopContext, dtSeconds: number) => void;
8
+ onTick?: (ctx: RuntimeWorldContext, dtSeconds: number) => void;
9
9
  };
10
10
  export type RuntimeInitContext = {
11
11
  snapshot: GameRuntimeSnapshot;
@@ -17,6 +17,9 @@ export type RuntimeLoopContext = RuntimeInitContext & {
17
17
  isNew: boolean;
18
18
  };
19
19
  };
20
+ export type RuntimeWorldContext = RuntimeInitContext & {
21
+ playerIds: readonly string[];
22
+ };
20
23
  export type GameRuntimeDefinition = {
21
24
  gameId: string;
22
25
  save: SaveConfig;
@@ -29,6 +32,7 @@ export type HydrateInput = {
29
32
  serverRow: RuntimeServerRow;
30
33
  playersByUserId: Record<string, RuntimePlayerRow>;
31
34
  chunksByKey: Record<string, RuntimeChunkRow>;
35
+ revision?: number;
32
36
  };
33
37
  export type GameRuntime = {
34
38
  gameId: string;
@@ -13,6 +13,7 @@ export function createGameRuntime(definition) {
13
13
  server: input.serverRow,
14
14
  players: input.playersByUserId,
15
15
  chunks: input.chunksByKey,
16
+ revision: input.revision,
16
17
  });
17
18
  if (loop?.onInit && !initialized) {
18
19
  initialized = true;
@@ -34,18 +35,18 @@ export function createGameRuntime(definition) {
34
35
  if (!loop?.onTick)
35
36
  return snapshot;
36
37
  let current = snapshot;
37
- for (const userId of Object.keys(current.players)) {
38
- const ctx = {
39
- get snapshot() {
40
- return current;
41
- },
42
- player: { userId, isNew: false },
43
- setSnapshot(next) {
44
- current = next;
45
- },
46
- };
47
- loop.onTick(ctx, dtSeconds);
48
- }
38
+ const ctx = {
39
+ get snapshot() {
40
+ return current;
41
+ },
42
+ get playerIds() {
43
+ return Object.keys(current.players);
44
+ },
45
+ setSnapshot(next) {
46
+ current = next;
47
+ },
48
+ };
49
+ loop.onTick(ctx, dtSeconds);
49
50
  return current;
50
51
  },
51
52
  joinPlayer(snapshot, userId, isNew) {
@@ -63,6 +63,7 @@ export declare function drainPendingLeaderboardIncrements(session: Record<string
63
63
  session: Record<string, unknown>;
64
64
  };
65
65
  export declare const LEADERBOARD_TOP_LIMIT = 100;
66
+ export declare function clampLimit(value: number | undefined, fallback: number, max: number): number;
66
67
  export type LeaderboardRow = {
67
68
  gameId: string;
68
69
  stat: string;
@@ -97,7 +98,12 @@ export type ServerListing = {
97
98
  tags?: string[];
98
99
  updatedAt: number;
99
100
  };
100
- export declare function toServerListing(record: GameServerRecord): ServerListing;
101
+ export type ToServerListingOptions = {
102
+ includeJoinCode?: boolean;
103
+ };
104
+ export declare function toServerListing(record: GameServerRecord, options?: ToServerListingOptions): ServerListing;
105
+ export declare const OPEN_SERVER_LISTING_LIMIT = 20;
106
+ export declare const OPEN_SERVER_LISTING_MAX = 100;
101
107
  export declare function toOpenServerListings(listings: Iterable<ServerListing>, limit?: number): ServerListing[];
102
108
  export type HostPersistence = {
103
109
  savePlan?: (plan: ServerPersistPlan) => Promise<void>;
@@ -112,6 +118,7 @@ export type HostPersistence = {
112
118
  saveProfile: (record: PlayerProfileRecord) => Promise<void>;
113
119
  loadChunks: (serverId: string) => Promise<WorldChunkRecord[]>;
114
120
  saveChunks: (serverId: string, chunks: WorldChunkRecord[]) => Promise<void>;
121
+ deleteChunks: (serverId: string, chunkKeys: string[]) => Promise<void>;
115
122
  loadFeed: (args: {
116
123
  serverId: string;
117
124
  action: string;
@@ -139,6 +146,7 @@ export type ServerPersistPlan = {
139
146
  server: GameServerRecord;
140
147
  profiles: PlayerProfileRecord[];
141
148
  chunks: WorldChunkRecord[];
149
+ deletedChunks: string[];
142
150
  leaderboard: LeaderboardIncrement[];
143
151
  };
144
152
  export declare function planServerPersist(server: GameServerRecord, snapshot: GameRuntimeSnapshot, save: SaveConfig, now: number): ServerPersistPlan;
@@ -21,6 +21,12 @@ export function drainPendingLeaderboardIncrements(session) {
21
21
  return { increments: raw.filter(isLeaderboardIncrement), session: rest };
22
22
  }
23
23
  export const LEADERBOARD_TOP_LIMIT = 100;
24
+ export function clampLimit(value, fallback, max) {
25
+ const candidate = value ?? fallback;
26
+ if (!Number.isFinite(candidate))
27
+ return fallback;
28
+ return Math.min(Math.max(Math.floor(candidate), 1), max);
29
+ }
24
30
  export function leaderboardRowKey(row) {
25
31
  return [row.gameId, row.scope, row.stat, row.serverId ?? "", row.userId].join("|");
26
32
  }
@@ -45,7 +51,7 @@ export function applyLeaderboardRows(rows, gameId, entries, now) {
45
51
  }
46
52
  }
47
53
  export function topLeaderboardRows(rows, args) {
48
- const limit = Math.min(Math.max(args.limit ?? LEADERBOARD_TOP_LIMIT, 1), LEADERBOARD_TOP_LIMIT);
54
+ const limit = clampLimit(args.limit, LEADERBOARD_TOP_LIMIT, LEADERBOARD_TOP_LIMIT);
49
55
  return [...rows]
50
56
  .filter((row) => row.gameId === args.gameId &&
51
57
  row.stat === args.stat &&
@@ -79,7 +85,7 @@ export function shouldAutoSave(save, dirtyAt, lastSavedAt, now) {
79
85
  const anchor = lastSavedAt ?? 0;
80
86
  return now - anchor >= intervalMs;
81
87
  }
82
- export function toServerListing(record) {
88
+ export function toServerListing(record, options = {}) {
83
89
  return {
84
90
  serverId: record.serverId,
85
91
  status: record.status,
@@ -88,16 +94,20 @@ export function toServerListing(record) {
88
94
  mode: record.mode,
89
95
  label: record.label,
90
96
  visibility: record.visibility,
91
- joinCode: record.joinCode,
97
+ ...(options.includeJoinCode === true && record.joinCode !== undefined
98
+ ? { joinCode: record.joinCode }
99
+ : {}),
92
100
  tags: record.tags,
93
101
  updatedAt: record.updatedAt,
94
102
  };
95
103
  }
96
- export function toOpenServerListings(listings, limit = 20) {
104
+ export const OPEN_SERVER_LISTING_LIMIT = 20;
105
+ export const OPEN_SERVER_LISTING_MAX = 100;
106
+ export function toOpenServerListings(listings, limit = OPEN_SERVER_LISTING_LIMIT) {
97
107
  return [...listings]
98
108
  .filter((listing) => listing.status === "running")
99
109
  .sort((a, b) => b.updatedAt - a.updatedAt)
100
- .slice(0, limit);
110
+ .slice(0, clampLimit(limit, OPEN_SERVER_LISTING_LIMIT, OPEN_SERVER_LISTING_MAX));
101
111
  }
102
112
  export function buildHydratePlayers(server, profiles) {
103
113
  const players = {};
@@ -146,12 +156,16 @@ export function planServerPersist(server, snapshot, save, now) {
146
156
  }
147
157
  }
148
158
  const chunks = [];
159
+ const deletedChunks = [];
149
160
  if (isSaveEnabled(save) && saveScopeIncludesChunks(save.scope)) {
150
161
  for (const chunkKey of snapshot.dirty.chunks) {
151
162
  const chunk = snapshot.chunks[chunkKey];
152
- if (!chunk)
153
- continue;
154
- chunks.push({ serverId: server.serverId, chunkKey, snapshot: chunk, updatedAt: now });
163
+ if (chunk) {
164
+ chunks.push({ serverId: server.serverId, chunkKey, snapshot: chunk, updatedAt: now });
165
+ }
166
+ else {
167
+ deletedChunks.push(chunkKey);
168
+ }
155
169
  }
156
170
  }
157
171
  return {
@@ -166,6 +180,7 @@ export function planServerPersist(server, snapshot, save, now) {
166
180
  },
167
181
  profiles,
168
182
  chunks,
183
+ deletedChunks,
169
184
  leaderboard,
170
185
  };
171
186
  }
@@ -0,0 +1,49 @@
1
+ import type { CommandResult } from "../commands/commandRegistry.js";
2
+ import type { GameDefinition, LoopPlayer } from "../game/defineGame.js";
3
+ import type { ModelAssetRef } from "../scene/assetCatalog.js";
4
+ import { type GameContext, type GameContextContent } from "./gameContext.js";
5
+ import { type InputFrame } from "./inputSnapshot.js";
6
+ import { type WorldDiff } from "./worldReplication.js";
7
+ import type { WorldSnapshot } from "./worldSnapshot.js";
8
+ export type { InputFrame };
9
+ /** Reserved command name the authoritative host intercepts on the existing `runCommand` transport to route a client's {@link InputFrame} to `session.input`, so per-tick input needs no separate wire. */
10
+ export declare const INPUT_COMMAND = "engine.input";
11
+ /** Config for {@link createHostedGameRunner}: the game definition, its content lookup, and an optional host identity. */
12
+ export interface HostedGameRunnerOptions<TAssetRef extends ModelAssetRef, TMultiplayer> {
13
+ definition: GameDefinition<TAssetRef, TMultiplayer>;
14
+ content: GameContextContent;
15
+ /** The world's own authoritative identity (`ctx.player` server-side); real players join as members. */
16
+ host?: LoopPlayer;
17
+ now?: () => number;
18
+ /**
19
+ * Rehydrate a persisted world instead of booting a fresh one — for stateless hosts (Convex) that reconstruct
20
+ * the runner each invocation. `onInit` still runs (so commands and systems it registers exist), then this
21
+ * snapshot overlays the world state it seeded. Omit for a long-lived stateful host (ws) that keeps one runner.
22
+ */
23
+ restore?: WorldSnapshot;
24
+ }
25
+ /**
26
+ * The GameContext-loop equivalent of the pure-reducer `createGameHost`: one authoritative `createGameContext`
27
+ * per world, driven server-side. `onInit` runs once at construction; `onNewPlayer`/`onPlayerLeave` fire per
28
+ * join/leave; `tick` advances `onTick` then commits a revision. Clients pull a full {@link WorldSnapshot}
29
+ * baseline once, then per-tick {@link WorldDiff}s from their last revision. Games ship only normal GameContext
30
+ * code — the runner adds no per-game surface.
31
+ */
32
+ export interface HostedGameRunner {
33
+ join(userId: string, isNew: boolean): void;
34
+ /** Re-attach an already-joined member on a reconstructed runner — restores membership and `ctx.game.players` without re-firing `onNewPlayer`. Stateless hosts call it once per persisted member after `restore`. */
35
+ resume(userId: string): void;
36
+ leave(userId: string): void;
37
+ /** Record a client's latest input frame — stashed for {@link heldInput} and mirrored onto `ctx.game.players` so the movement seam reads each player's intent in `onTick`. */
38
+ input(userId: string, frame: InputFrame): void;
39
+ heldInput(userId: string): InputFrame | null;
40
+ command(userId: string, name: string, input: unknown): CommandResult<GameContext>;
41
+ tick(dt: number): number;
42
+ diff(sinceRevision: number): WorldDiff;
43
+ revision(): number;
44
+ snapshot(): WorldSnapshot;
45
+ members(): readonly string[];
46
+ context(): GameContext;
47
+ }
48
+ /** Build a {@link HostedGameRunner} — one authoritative GameContext world driven server-side from the game's own loop. */
49
+ export declare function createHostedGameRunner<TAssetRef extends ModelAssetRef, TMultiplayer>(options: HostedGameRunnerOptions<TAssetRef, TMultiplayer>): HostedGameRunner;
@@ -0,0 +1,62 @@
1
+ import { createGameContext } from "./gameContext.js";
2
+ import {} from "./inputSnapshot.js";
3
+ import { createWorldReplicator } from "./worldReplication.js";
4
+ /** Reserved command name the authoritative host intercepts on the existing `runCommand` transport to route a client's {@link InputFrame} to `session.input`, so per-tick input needs no separate wire. */
5
+ export const INPUT_COMMAND = "engine.input";
6
+ /** Build a {@link HostedGameRunner} — one authoritative GameContext world driven server-side from the game's own loop. */
7
+ export function createHostedGameRunner(options) {
8
+ const { definition, content, host, now, restore } = options;
9
+ const ctx = createGameContext({
10
+ definition,
11
+ content,
12
+ player: host ?? { userId: "host", isNew: true },
13
+ ...(now === undefined ? {} : { now }),
14
+ });
15
+ const loop = definition.loop ?? {};
16
+ const replicator = createWorldReplicator(() => ctx.snapshot());
17
+ const members = new Map();
18
+ const inputs = new Map();
19
+ loop.onInit?.(ctx);
20
+ if (restore !== undefined)
21
+ ctx.hydrate(restore);
22
+ return {
23
+ join(userId, isNew) {
24
+ const player = { userId, isNew };
25
+ members.set(userId, player);
26
+ ctx.game.players?.join(userId, isNew);
27
+ loop.onNewPlayer?.(ctx, player);
28
+ },
29
+ resume(userId) {
30
+ members.set(userId, { userId, isNew: false });
31
+ ctx.game.players?.join(userId, false);
32
+ },
33
+ leave(userId) {
34
+ const player = members.get(userId);
35
+ if (player === undefined)
36
+ return;
37
+ members.delete(userId);
38
+ inputs.delete(userId);
39
+ loop.onPlayerLeave?.(ctx, player);
40
+ ctx.game.players?.leave(userId);
41
+ },
42
+ input(userId, frame) {
43
+ inputs.set(userId, frame);
44
+ ctx.game.players?.setInput(userId, frame);
45
+ },
46
+ heldInput(userId) {
47
+ return inputs.get(userId) ?? null;
48
+ },
49
+ command(userId, name, input) {
50
+ return ctx.game.commands.runAs(userId, name, input);
51
+ },
52
+ tick(dt) {
53
+ loop.onTick?.(ctx, dt);
54
+ return replicator.commit();
55
+ },
56
+ diff: (sinceRevision) => replicator.diff(sinceRevision),
57
+ revision: () => replicator.revision(),
58
+ snapshot: () => ctx.snapshot(),
59
+ members: () => Array.from(members.keys()),
60
+ context: () => ctx,
61
+ };
62
+ }
@@ -0,0 +1,65 @@
1
+ import type { CommandResult } from "../commands/commandRegistry.js";
2
+ import type { ModelAssetRef } from "../scene/assetCatalog.js";
3
+ import type { GameContext, GameContextContent } from "./gameContext.js";
4
+ import type { GameDefinition, LoopPlayer } from "../game/defineGame.js";
5
+ import { type HostedGameRunner, type InputFrame } from "./hostedGameRunner.js";
6
+ import type { WorldDiff } from "./worldReplication.js";
7
+ import type { WorldSnapshot } from "./worldSnapshot.js";
8
+ /** One hosted world's persisted authoritative state — the unit a {@link HostedWorldStore} loads and saves. */
9
+ export interface HostedWorldRecord {
10
+ snapshot: WorldSnapshot;
11
+ revision: number;
12
+ }
13
+ /**
14
+ * Narrow persistence seam for a hosted world — the {@link HostedWorldRecord} counterpart of `HostPersistence`.
15
+ * Backends implement it (memory/file/sql/convex); the session never names one. A stateful host loads once and
16
+ * saves on a cadence; a stateless host reconstructs from `load()` each invocation.
17
+ */
18
+ export interface HostedWorldStore {
19
+ load(): HostedWorldRecord | null;
20
+ save(record: HostedWorldRecord): void;
21
+ }
22
+ /** In-process {@link HostedWorldStore} for tests, local play, and the browser-tab P2P host. */
23
+ export declare function memoryWorldStore(seed?: HostedWorldRecord): HostedWorldStore;
24
+ /** A client replication pull: a full baseline (first sync / fell behind) or a diff since the client's cursor. */
25
+ export type HostedWorldSync = {
26
+ kind: "baseline";
27
+ revision: number;
28
+ snapshot: WorldSnapshot;
29
+ } | {
30
+ kind: "diff";
31
+ diff: WorldDiff;
32
+ };
33
+ /** Config for {@link createHostedWorldSession}: the game, its persistence store, and the auto-save cadence. */
34
+ export interface HostedWorldSessionOptions<TAssetRef extends ModelAssetRef, TMultiplayer> {
35
+ definition: GameDefinition<TAssetRef, TMultiplayer>;
36
+ content: GameContextContent;
37
+ host?: LoopPlayer;
38
+ now?: () => number;
39
+ /** Where the authoritative snapshot persists; defaults to {@link memoryWorldStore}. */
40
+ store?: HostedWorldStore;
41
+ /** Minimum elapsed `now()` ms between auto-saves on tick. Default `0` — persist on every revision-changing tick. */
42
+ saveIntervalMs?: number;
43
+ }
44
+ /**
45
+ * The stateful host substrate a long-lived backend binds (ws server, browser P2P host): a live
46
+ * {@link HostedGameRunner} loaded from a {@link HostedWorldStore}, auto-persisted on tick, serving each client a
47
+ * baseline then diffs. The stateless Convex path doesn't use this — it reconstructs a runner from the same store
48
+ * per invocation and diffs with `diffSnapshots`. Either way the store seam and the game code are identical.
49
+ */
50
+ export interface HostedWorldSession {
51
+ join(userId: string, isNew: boolean): void;
52
+ leave(userId: string): void;
53
+ input(userId: string, frame: InputFrame): void;
54
+ command(userId: string, name: string, input: unknown): CommandResult<GameContext>;
55
+ tick(dt: number): number;
56
+ /** Replication pull: `null`/`0` cursor → full baseline; a prior revision → a diff since it. */
57
+ sync(sinceRevision: number | null): HostedWorldSync;
58
+ revision(): number;
59
+ members(): readonly string[];
60
+ /** Force-persist the current world to the store. */
61
+ save(): void;
62
+ runner(): HostedGameRunner;
63
+ }
64
+ /** Build a {@link HostedWorldSession} — a live runner loaded from a {@link HostedWorldStore} and auto-persisted on tick. */
65
+ export declare function createHostedWorldSession<TAssetRef extends ModelAssetRef, TMultiplayer>(options: HostedWorldSessionOptions<TAssetRef, TMultiplayer>): HostedWorldSession;
@@ -0,0 +1,57 @@
1
+ import { createHostedGameRunner } from "./hostedGameRunner.js";
2
+ /** In-process {@link HostedWorldStore} for tests, local play, and the browser-tab P2P host. */
3
+ export function memoryWorldStore(seed) {
4
+ let record = seed ?? null;
5
+ return {
6
+ load: () => record,
7
+ save(next) {
8
+ record = { snapshot: next.snapshot, revision: next.revision };
9
+ },
10
+ };
11
+ }
12
+ /** Build a {@link HostedWorldSession} — a live runner loaded from a {@link HostedWorldStore} and auto-persisted on tick. */
13
+ export function createHostedWorldSession(options) {
14
+ const { definition, content, host, now, saveIntervalMs = 0 } = options;
15
+ const store = options.store ?? memoryWorldStore();
16
+ const loaded = store.load();
17
+ const runner = createHostedGameRunner({
18
+ definition,
19
+ content,
20
+ ...(host === undefined ? {} : { host }),
21
+ ...(now === undefined ? {} : { now }),
22
+ ...(loaded === null ? {} : { restore: loaded.snapshot }),
23
+ });
24
+ let savedRevision = loaded?.revision ?? 0;
25
+ let lastSaveAt = now?.() ?? 0;
26
+ function persist() {
27
+ savedRevision = runner.revision();
28
+ store.save({ snapshot: runner.snapshot(), revision: savedRevision });
29
+ }
30
+ return {
31
+ join: (userId, isNew) => runner.join(userId, isNew),
32
+ leave: (userId) => runner.leave(userId),
33
+ input: (userId, frame) => runner.input(userId, frame),
34
+ command: (userId, name, input) => runner.command(userId, name, input),
35
+ tick(dt) {
36
+ const revision = runner.tick(dt);
37
+ if (revision !== savedRevision) {
38
+ const at = now?.() ?? 0;
39
+ if (saveIntervalMs <= 0 || at - lastSaveAt >= saveIntervalMs) {
40
+ lastSaveAt = at;
41
+ persist();
42
+ }
43
+ }
44
+ return revision;
45
+ },
46
+ sync(sinceRevision) {
47
+ if (sinceRevision === null || sinceRevision <= 0) {
48
+ return { kind: "baseline", revision: runner.revision(), snapshot: runner.snapshot() };
49
+ }
50
+ return { kind: "diff", diff: runner.diff(sinceRevision) };
51
+ },
52
+ revision: () => runner.revision(),
53
+ members: () => runner.members(),
54
+ save: persist,
55
+ runner: () => runner,
56
+ };
57
+ }
@@ -1,7 +1,25 @@
1
+ import type { PointerAxisState } from "../input/pointerAxis.js";
2
+ import { type AxisBinding, type AxisRange } from "../input/axisInput.js";
3
+ /** One client's input for a tick — the semantic held-action set plus pointer state, the serializable, over-the-wire counterpart of {@link InputSnapshot} the host stores per connected player. */
4
+ export interface InputFrame {
5
+ held: readonly string[];
6
+ pointer: PointerAxisState | null;
7
+ }
1
8
  export interface InputSnapshot {
2
9
  /** Replaces the held-action set for this frame. Called by the shell before `onTick` each frame; does not bump `ctx.version()` or notify `ctx.subscribe` listeners — per-frame publishes would storm subscribers. */
3
10
  publish(held: readonly string[]): void;
11
+ /** Replaces the normalized pointer-position state for this frame (#293). Same no-notify contract as `publish`. */
12
+ publishPointer(state: PointerAxisState | null): void;
4
13
  isDown(action: string): boolean;
5
14
  held(): readonly string[];
15
+ /** Pointer position over the play surface, `[-1, 1]` per axis with `+y` down, published by the shell each frame; `null` until the first pointer move. */
16
+ pointer(): PointerAxisState | null;
17
+ /**
18
+ * Instantaneous analog axis sample against the held-action set and current pointer (#533.7) — bind
19
+ * throttle/steer/handbrake (or any axes) to *action names*, not raw key codes, since the held set is
20
+ * semantic actions. Feed the result into an `AxisChannel` for smoothing; unlisted ranges default to
21
+ * bipolar `[-1, 1]`, so pass `{ min: 0, max: 1 }` for one-directional pedals.
22
+ */
23
+ axis<TAxes extends string>(bindings: Record<TAxes, AxisBinding>, ranges?: Partial<Record<TAxes, AxisRange>>): Record<TAxes, number>;
6
24
  }
7
25
  export declare function createInputSnapshot(): InputSnapshot;
@@ -1,12 +1,19 @@
1
+ import { sampleAxisBindings } from "../input/axisInput.js";
1
2
  export function createInputSnapshot() {
2
3
  let heldSet = new Set();
3
4
  let heldList = [];
5
+ let pointerState = null;
4
6
  return {
5
7
  publish(held) {
6
8
  heldList = held;
7
9
  heldSet = new Set(held);
8
10
  },
11
+ publishPointer(state) {
12
+ pointerState = state;
13
+ },
9
14
  isDown: (action) => heldSet.has(action),
10
15
  held: () => heldList,
16
+ pointer: () => pointerState,
17
+ axis: (bindings, ranges) => sampleAxisBindings(bindings, (action) => heldSet.has(action), pointerState, ranges),
11
18
  };
12
19
  }
@@ -1,11 +1,14 @@
1
1
  /**
2
- * Seam for game code to reach the vertical motion the shell's FrameDriver
2
+ * Seam for game code to reach the motion the shell's FrameDriver
3
3
  * otherwise owns privately (#162.4). Game code calls `impulse`,
4
- * `setVerticalVelocity`, and/or `setY` from `onTick` or commands; the shell
5
- * calls `takePending()` once per frame, before integrating gravity, to drain
6
- * what accumulated. `setY` wins over physics for that frame; impulses add to
7
- * the vertical velocity the driver is about to integrate; a later
8
- * `setVerticalVelocity` replaces that velocity outright.
4
+ * `pushHorizontal`, `setVerticalVelocity`, and/or `setY` from `onTick` or
5
+ * commands; the shell calls `takePending()` once per frame, before
6
+ * integrating gravity, to drain what accumulated. `setY` wins over physics
7
+ * for that frame; impulses add to the velocity the driver is about to
8
+ * integrate; a later `setVerticalVelocity` replaces that velocity outright.
9
+ * Horizontal pushes compose with the walk controller (#282.4): they add to
10
+ * its horizontal velocity and decay naturally as it re-blends toward input —
11
+ * knockback, dashes, explosion shoves without raw `setPose` offsets.
9
12
  *
10
13
  * Intentionally not `ctx.version()`/`ctx.subscribe` wired: these fire at
11
14
  * most once per frame and would otherwise storm subscribers with per-frame
@@ -13,13 +16,20 @@
13
16
  */
14
17
  export interface MotionIntents {
15
18
  impulse(velocityY: number): void;
19
+ /** Adds a horizontal velocity kick (world units/s) that the walk controller integrates and decays. */
20
+ pushHorizontal(velocityX: number, velocityZ: number): void;
16
21
  setVerticalVelocity(velocityY: number): void;
17
22
  setY(y: number): void;
18
23
  takePending(): MotionIntentBatch | null;
19
24
  }
20
25
  export interface MotionIntentBatch {
21
26
  impulses: readonly number[];
27
+ horizontalImpulses: readonly (readonly [number, number])[];
22
28
  verticalVelocity: number | null;
23
29
  y: number | null;
24
30
  }
25
31
  export declare function createMotionIntents(): MotionIntents;
32
+ /** Fold a batch's vertical impulses into a controller's velocity, then apply an outright `setVerticalVelocity` override — the vertical counterpart of {@link applyHorizontalImpulses}. */
33
+ export declare function applyMotionImpulses(currentVelocity: number, batch: MotionIntentBatch | null): number;
34
+ /** Fold a batch's horizontal pushes into a controller's velocity pair — shared by the walk and voxel drivers. */
35
+ export declare function applyHorizontalImpulses(velocityX: number, velocityZ: number, batch: MotionIntentBatch | null): readonly [number, number];
@@ -1,11 +1,15 @@
1
1
  export function createMotionIntents() {
2
2
  let impulses = [];
3
+ let horizontalImpulses = [];
3
4
  let verticalVelocity = null;
4
5
  let y = null;
5
6
  return {
6
7
  impulse(velocityY) {
7
8
  impulses.push(velocityY);
8
9
  },
10
+ pushHorizontal(velocityX, velocityZ) {
11
+ horizontalImpulses.push([velocityX, velocityZ]);
12
+ },
9
13
  setVerticalVelocity(velocityY) {
10
14
  verticalVelocity = velocityY;
11
15
  },
@@ -13,13 +17,38 @@ export function createMotionIntents() {
13
17
  y = nextY;
14
18
  },
15
19
  takePending() {
16
- if (impulses.length === 0 && verticalVelocity === null && y === null)
20
+ if (impulses.length === 0 && horizontalImpulses.length === 0 && verticalVelocity === null && y === null) {
17
21
  return null;
18
- const batch = { impulses, verticalVelocity, y };
22
+ }
23
+ const batch = { impulses, horizontalImpulses, verticalVelocity, y };
19
24
  impulses = [];
25
+ horizontalImpulses = [];
20
26
  verticalVelocity = null;
21
27
  y = null;
22
28
  return batch;
23
29
  },
24
30
  };
25
31
  }
32
+ /** Fold a batch's vertical impulses into a controller's velocity, then apply an outright `setVerticalVelocity` override — the vertical counterpart of {@link applyHorizontalImpulses}. */
33
+ export function applyMotionImpulses(currentVelocity, batch) {
34
+ if (batch === null)
35
+ return currentVelocity;
36
+ let velocity = currentVelocity;
37
+ for (const impulse of batch.impulses)
38
+ velocity += impulse;
39
+ if (batch.verticalVelocity !== null)
40
+ velocity = batch.verticalVelocity;
41
+ return velocity;
42
+ }
43
+ /** Fold a batch's horizontal pushes into a controller's velocity pair — shared by the walk and voxel drivers. */
44
+ export function applyHorizontalImpulses(velocityX, velocityZ, batch) {
45
+ if (batch === null || batch.horizontalImpulses.length === 0)
46
+ return [velocityX, velocityZ];
47
+ let x = velocityX;
48
+ let z = velocityZ;
49
+ for (const [ix, iz] of batch.horizontalImpulses) {
50
+ x += ix;
51
+ z += iz;
52
+ }
53
+ return [x, z];
54
+ }
@@ -77,6 +77,7 @@ export declare function createRuntimeSnapshot(args: {
77
77
  server?: RuntimeServerRow;
78
78
  players?: Record<string, RuntimePlayerRow>;
79
79
  chunks?: Record<string, RuntimeChunkRow>;
80
+ revision?: number;
80
81
  }): GameRuntimeSnapshot;
81
82
  export declare function markServerDirty(snapshot: GameRuntimeSnapshot): GameRuntimeSnapshot;
82
83
  export declare function markPlayerDirty(snapshot: GameRuntimeSnapshot, userId: string): GameRuntimeSnapshot;
@@ -24,7 +24,7 @@ export function createRuntimeSnapshot(args) {
24
24
  server: args.server ?? createEmptyServerRow(),
25
25
  players: args.players ?? {},
26
26
  chunks: args.chunks ?? {},
27
- revision: 0,
27
+ revision: args.revision ?? 0,
28
28
  dirty: {
29
29
  server: false,
30
30
  players: [],
@@ -0,0 +1,74 @@
1
+ /** Stable identifier for a visibility-tracked object or camera. */
2
+ export type VisibilityId = string | number;
3
+ /** World-space point; `z` is optional for 2D adapters. */
4
+ export interface VisibilityPoint {
5
+ x: number;
6
+ y: number;
7
+ z?: number;
8
+ }
9
+ /** Sphere bounds used for visibility/streaming distance checks. */
10
+ export interface VisibilityBounds {
11
+ center: VisibilityPoint;
12
+ radius: number;
13
+ }
14
+ /** A camera's position and reach, as seen by the visibility system. */
15
+ export interface CameraVisibilityContext {
16
+ id: VisibilityId;
17
+ position: VisibilityPoint;
18
+ /** Approximate visible radius in world units. Adapters may derive this from a frustum. */
19
+ visibleRadius: number;
20
+ /** Extra distance used to request assets before an object becomes visible. */
21
+ preloadMargin?: number;
22
+ /** Cameras such as editor thumbnails can opt out of influencing streaming. */
23
+ influencesStreaming?: boolean;
24
+ }
25
+ /** Per-object escape hatches that bypass or tune the default visibility policy. */
26
+ export interface VisibilityOverrides {
27
+ alwaysVisible?: boolean;
28
+ neverUnload?: boolean;
29
+ cullingDisabled?: boolean;
30
+ streamingDisabled?: boolean;
31
+ minRenderDistance?: number;
32
+ maxRenderDistance?: number;
33
+ preloadMargin?: number;
34
+ }
35
+ /** An entity/asset tracked by `VisibilitySystem.evaluate`. */
36
+ export interface VisibilityObject {
37
+ id: VisibilityId;
38
+ bounds: VisibilityBounds;
39
+ overrides?: VisibilityOverrides;
40
+ }
41
+ /** Baseline policy settings a `VisibilitySystem` is constructed with. */
42
+ export interface VisibilityDefaults {
43
+ enabled: boolean;
44
+ streamingEnabled: boolean;
45
+ minRenderDistance: number;
46
+ maxRenderDistance: number;
47
+ preloadMargin: number;
48
+ unloadGraceMs: number;
49
+ hysteresis: number;
50
+ }
51
+ /** Default `VisibilityDefaults` a `VisibilitySystem` falls back to when unspecified. */
52
+ export declare const DEFAULT_VISIBILITY_SETTINGS: Readonly<VisibilityDefaults>;
53
+ /** Result of evaluating one object against the active cameras this tick. */
54
+ export interface VisibilityDecision {
55
+ render: boolean;
56
+ load: boolean;
57
+ unload: boolean;
58
+ reason: "always-visible" | "culling-disabled" | "visible" | "preload" | "distance-culled" | "outside-view" | "grace-period" | "streaming-disabled";
59
+ }
60
+ /**
61
+ * Engine-level visibility and asset-residency policy.
62
+ *
63
+ * Render culling and asset streaming are intentionally separate. This class never
64
+ * destroys entities or throttles simulation; adapters decide how to apply results.
65
+ */
66
+ export declare class VisibilitySystem {
67
+ #private;
68
+ readonly settings: VisibilityDefaults;
69
+ constructor(settings?: Partial<VisibilityDefaults>);
70
+ evaluate(object: VisibilityObject, cameras: readonly CameraVisibilityContext[], now?: number): VisibilityDecision;
71
+ setLoaded(id: VisibilityId, loaded: boolean, now?: number): void;
72
+ forget(id: VisibilityId): void;
73
+ clear(): void;
74
+ }