@jgengine/core 0.7.0 → 0.9.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 (329) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +2 -0
  3. package/dist/ai/flock.d.ts +36 -0
  4. package/dist/ai/flock.js +182 -0
  5. package/dist/ai/groupAssist.d.ts +19 -0
  6. package/dist/ai/groupAssist.js +59 -0
  7. package/dist/ai/laneSelect.d.ts +21 -0
  8. package/dist/ai/laneSelect.js +37 -0
  9. package/dist/ai/mobBrain.d.ts +68 -0
  10. package/dist/ai/mobBrain.js +115 -0
  11. package/dist/ai/spawnDirector.d.ts +5 -0
  12. package/dist/ai/spawnDirector.js +14 -2
  13. package/dist/ai/threat.d.ts +2 -0
  14. package/dist/ai/threat.js +34 -0
  15. package/dist/anim/easing.d.ts +14 -0
  16. package/dist/anim/easing.js +55 -0
  17. package/dist/behaviour/behaviour.d.ts +136 -0
  18. package/dist/behaviour/behaviour.js +388 -0
  19. package/dist/cards/cardPile.d.ts +1 -0
  20. package/dist/cards/cardPile.js +12 -3
  21. package/dist/cartridge/runtime.d.ts +15 -0
  22. package/dist/cartridge/runtime.js +556 -0
  23. package/dist/cartridge/spec.d.ts +188 -0
  24. package/dist/cartridge/spec.js +19 -0
  25. package/dist/cartridge/validate.d.ts +2 -0
  26. package/dist/cartridge/validate.js +78 -0
  27. package/dist/combat/abilityKit.d.ts +22 -2
  28. package/dist/combat/abilityKit.js +72 -13
  29. package/dist/combat/castRunner.d.ts +40 -0
  30. package/dist/combat/castRunner.js +56 -0
  31. package/dist/combat/comboPoints.d.ts +14 -0
  32. package/dist/combat/comboPoints.js +52 -0
  33. package/dist/combat/defensiveWindow.d.ts +1 -1
  34. package/dist/combat/defensiveWindow.js +5 -3
  35. package/dist/combat/effects.d.ts +1 -1
  36. package/dist/combat/effects.js +9 -6
  37. package/dist/combat/projectiles.d.ts +64 -6
  38. package/dist/combat/projectiles.js +232 -74
  39. package/dist/combat/resistance.d.ts +16 -4
  40. package/dist/combat/resistance.js +43 -7
  41. package/dist/combat/resourcePool.d.ts +20 -0
  42. package/dist/combat/resourcePool.js +55 -0
  43. package/dist/combat/shotOrigin.d.ts +40 -0
  44. package/dist/combat/shotOrigin.js +57 -0
  45. package/dist/combat/telegraph.d.ts +30 -1
  46. package/dist/combat/telegraph.js +61 -0
  47. package/dist/commands/commandRegistry.d.ts +1 -1
  48. package/dist/commands/commandRegistry.js +2 -1
  49. package/dist/crafting/recipe.js +4 -1
  50. package/dist/data/dataSource.d.ts +26 -0
  51. package/dist/data/dataSource.js +93 -0
  52. package/dist/data/devProxy.d.ts +11 -0
  53. package/dist/data/devProxy.js +38 -0
  54. package/dist/data/fetchJson.d.ts +20 -0
  55. package/dist/data/fetchJson.js +43 -0
  56. package/dist/data/jsonDataSource.d.ts +4 -0
  57. package/dist/data/jsonDataSource.js +6 -0
  58. package/dist/devtools/devtools.d.ts +205 -0
  59. package/dist/devtools/devtools.js +832 -0
  60. package/dist/devtools/transformTunables.d.ts +14 -0
  61. package/dist/devtools/transformTunables.js +38 -0
  62. package/dist/devtools/tunableSchema.d.ts +140 -0
  63. package/dist/devtools/tunableSchema.js +476 -0
  64. package/dist/faction/factions.d.ts +33 -0
  65. package/dist/faction/factions.js +76 -0
  66. package/dist/faction/reputation.d.ts +32 -0
  67. package/dist/faction/reputation.js +82 -0
  68. package/dist/game/chat.d.ts +68 -0
  69. package/dist/game/chat.js +173 -0
  70. package/dist/game/chatFilter.d.ts +15 -0
  71. package/dist/game/chatFilter.js +55 -0
  72. package/dist/game/controlGate.d.ts +4 -0
  73. package/dist/game/controlGate.js +7 -0
  74. package/dist/game/defineGame.d.ts +10 -2
  75. package/dist/game/defineGame.js +2 -1
  76. package/dist/game/events.d.ts +35 -0
  77. package/dist/game/gamePhase.d.ts +20 -0
  78. package/dist/game/gamePhase.js +21 -0
  79. package/dist/game/levelSequence.d.ts +44 -0
  80. package/dist/game/levelSequence.js +78 -0
  81. package/dist/game/lootTable.d.ts +10 -1
  82. package/dist/game/lootTable.js +27 -7
  83. package/dist/game/playableGame.d.ts +286 -6
  84. package/dist/game/playableGame.js +6 -1
  85. package/dist/game/progression.d.ts +1 -0
  86. package/dist/game/progression.js +23 -2
  87. package/dist/game/quest.d.ts +7 -3
  88. package/dist/game/quest.js +17 -8
  89. package/dist/game/race.d.ts +44 -1
  90. package/dist/game/race.js +116 -11
  91. package/dist/game/recordBook.d.ts +29 -0
  92. package/dist/game/recordBook.js +86 -0
  93. package/dist/game/social.d.ts +42 -0
  94. package/dist/game/social.js +122 -0
  95. package/dist/game/spawnPoints.d.ts +19 -0
  96. package/dist/game/spawnPoints.js +23 -0
  97. package/dist/game/talents.d.ts +47 -0
  98. package/dist/game/talents.js +140 -0
  99. package/dist/game/worldItem.d.ts +3 -0
  100. package/dist/game/worldItem.js +12 -6
  101. package/dist/index.d.ts +1 -0
  102. package/dist/index.js +1 -0
  103. package/dist/input/actionBindings.d.ts +10 -0
  104. package/dist/input/actionBindings.js +14 -0
  105. package/dist/input/axisInput.d.ts +26 -3
  106. package/dist/input/axisInput.js +44 -4
  107. package/dist/input/bindingOverrides.d.ts +25 -0
  108. package/dist/input/bindingOverrides.js +95 -0
  109. package/dist/input/gestureSurface.d.ts +31 -0
  110. package/dist/input/gestureSurface.js +120 -0
  111. package/dist/input/pointer.d.ts +45 -0
  112. package/dist/input/pointer.js +48 -0
  113. package/dist/input/pointerAxis.d.ts +30 -0
  114. package/dist/input/pointerAxis.js +31 -0
  115. package/dist/input/touchScheme.d.ts +89 -0
  116. package/dist/input/touchScheme.js +153 -0
  117. package/dist/item/affix.d.ts +1 -1
  118. package/dist/item/affix.js +1 -19
  119. package/dist/meta/changelog.d.ts +1 -1
  120. package/dist/meta/changelog.js +59 -1
  121. package/dist/movement/dash.d.ts +3 -0
  122. package/dist/movement/dash.js +19 -5
  123. package/dist/movement/glideModel.d.ts +44 -0
  124. package/dist/movement/glideModel.js +54 -0
  125. package/dist/movement/grappleSwing.d.ts +34 -0
  126. package/dist/movement/grappleSwing.js +56 -0
  127. package/dist/movement/leaderTrail.d.ts +29 -0
  128. package/dist/movement/leaderTrail.js +67 -0
  129. package/dist/movement/movementModel.d.ts +36 -3
  130. package/dist/movement/movementModel.js +81 -5
  131. package/dist/movement/steering.d.ts +27 -0
  132. package/dist/movement/steering.js +40 -0
  133. package/dist/movement/voxelController.d.ts +61 -0
  134. package/dist/movement/voxelController.js +229 -0
  135. package/dist/multiplayer/chatContract.d.ts +42 -0
  136. package/dist/multiplayer/chatContract.js +57 -0
  137. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  138. package/dist/multiplayer/feedWriteGate.js +12 -0
  139. package/dist/multiplayer/identity.d.ts +13 -0
  140. package/dist/multiplayer/identity.js +20 -0
  141. package/dist/multiplayer/poseSyncGate.d.ts +1 -0
  142. package/dist/multiplayer/poseSyncGate.js +13 -1
  143. package/dist/multiplayer/presenceModel.d.ts +17 -2
  144. package/dist/multiplayer/presenceModel.js +30 -4
  145. package/dist/multiplayer/voiceContract.d.ts +44 -0
  146. package/dist/multiplayer/voiceContract.js +108 -0
  147. package/dist/nav/corridors.d.ts +30 -0
  148. package/dist/nav/corridors.js +69 -0
  149. package/dist/nav/navConstrain.d.ts +14 -0
  150. package/dist/nav/navConstrain.js +26 -0
  151. package/dist/nav/navFromEnvironment.d.ts +11 -0
  152. package/dist/nav/navFromEnvironment.js +16 -0
  153. package/dist/nav/navGrid.d.ts +10 -0
  154. package/dist/nav/navGrid.js +48 -5
  155. package/dist/nav/pathFollow.d.ts +4 -2
  156. package/dist/nav/pathFollow.js +5 -3
  157. package/dist/nav/railGraph.d.ts +66 -0
  158. package/dist/nav/railGraph.js +129 -0
  159. package/dist/nav/timetable.d.ts +41 -0
  160. package/dist/nav/timetable.js +94 -0
  161. package/dist/physics/ballisticSweep.d.ts +18 -0
  162. package/dist/physics/ballisticSweep.js +49 -0
  163. package/dist/physics/buoyancy.d.ts +6 -0
  164. package/dist/physics/buoyancy.js +14 -0
  165. package/dist/physics/flowTube.d.ts +32 -0
  166. package/dist/physics/flowTube.js +64 -0
  167. package/dist/physics/forceVolume.d.ts +27 -0
  168. package/dist/physics/forceVolume.js +43 -5
  169. package/dist/physics/kinematicVehicle.d.ts +52 -0
  170. package/dist/physics/kinematicVehicle.js +81 -0
  171. package/dist/physics/physicsWorld.d.ts +54 -8
  172. package/dist/physics/physicsWorld.js +400 -79
  173. package/dist/physics/ragdoll.js +1 -1
  174. package/dist/physics/spatialGrid.d.ts +1 -1
  175. package/dist/physics/spatialGrid.js +5 -0
  176. package/dist/physics/vehicleBody.d.ts +6 -0
  177. package/dist/physics/vehicleBody.js +18 -18
  178. package/dist/puzzle/cellGrid.d.ts +26 -0
  179. package/dist/puzzle/cellGrid.js +110 -0
  180. package/dist/puzzle/fallingPiece.d.ts +24 -0
  181. package/dist/puzzle/fallingPiece.js +51 -0
  182. package/dist/random/nameGen.d.ts +18 -0
  183. package/dist/random/nameGen.js +35 -0
  184. package/dist/random/rng.d.ts +2 -0
  185. package/dist/random/rng.js +23 -0
  186. package/dist/random/seedLink.d.ts +5 -0
  187. package/dist/random/seedLink.js +35 -0
  188. package/dist/runtime/adapter.d.ts +35 -0
  189. package/dist/runtime/adapter.js +37 -1
  190. package/dist/runtime/cameraDirector.d.ts +15 -0
  191. package/dist/runtime/cameraDirector.js +31 -0
  192. package/dist/runtime/commandRunner.d.ts +2 -2
  193. package/dist/runtime/commandRunner.js +2 -2
  194. package/dist/runtime/gameContext.d.ts +65 -2
  195. package/dist/runtime/gameContext.js +280 -9
  196. package/dist/runtime/gameRuntime.d.ts +5 -1
  197. package/dist/runtime/gameRuntime.js +13 -12
  198. package/dist/runtime/hostPersistence.d.ts +30 -1
  199. package/dist/runtime/hostPersistence.js +70 -5
  200. package/dist/runtime/inputSnapshot.d.ts +12 -0
  201. package/dist/runtime/inputSnapshot.js +17 -0
  202. package/dist/runtime/motionIntents.d.ts +33 -0
  203. package/dist/runtime/motionIntents.js +43 -0
  204. package/dist/runtime/snapshot.d.ts +1 -0
  205. package/dist/runtime/snapshot.js +1 -1
  206. package/dist/runtime/transport.d.ts +32 -0
  207. package/dist/runtime/visibility.d.ts +74 -0
  208. package/dist/runtime/visibility.js +127 -0
  209. package/dist/scene/colliders.d.ts +44 -0
  210. package/dist/scene/colliders.js +77 -0
  211. package/dist/scene/entityStore.d.ts +30 -5
  212. package/dist/scene/entityStore.js +83 -13
  213. package/dist/scene/movementSpeed.d.ts +35 -0
  214. package/dist/scene/movementSpeed.js +20 -0
  215. package/dist/scene/objectQuery.d.ts +29 -0
  216. package/dist/scene/objectQuery.js +140 -0
  217. package/dist/scene/objectStore.d.ts +13 -0
  218. package/dist/scene/objectStore.js +130 -6
  219. package/dist/scene/paintLayer.d.ts +15 -0
  220. package/dist/scene/paintLayer.js +53 -0
  221. package/dist/scene/sceneRaycast.d.ts +80 -0
  222. package/dist/scene/sceneRaycast.js +284 -0
  223. package/dist/scene/spatial.d.ts +17 -1
  224. package/dist/scene/spatial.js +140 -17
  225. package/dist/sensor/concealment.d.ts +21 -0
  226. package/dist/sensor/concealment.js +63 -0
  227. package/dist/sensor/freezeMonitor.d.ts +17 -0
  228. package/dist/sensor/freezeMonitor.js +35 -0
  229. package/dist/sensor/recordingBuffer.d.ts +6 -0
  230. package/dist/sensor/recordingBuffer.js +7 -0
  231. package/dist/sensor/replayLoop.d.ts +39 -0
  232. package/dist/sensor/replayLoop.js +64 -0
  233. package/dist/sensor/visionCone.d.ts +31 -0
  234. package/dist/sensor/visionCone.js +72 -0
  235. package/dist/session/extraction.d.ts +2 -2
  236. package/dist/session/extraction.js +13 -2
  237. package/dist/session/roles.d.ts +6 -0
  238. package/dist/session/roles.js +32 -0
  239. package/dist/session/roundState.d.ts +41 -17
  240. package/dist/session/roundState.js +67 -38
  241. package/dist/settings/settingsModel.d.ts +97 -0
  242. package/dist/settings/settingsModel.js +100 -0
  243. package/dist/stats/statModifiers.d.ts +5 -2
  244. package/dist/stats/statModifiers.js +32 -14
  245. package/dist/store/observableKeyedStore.js +8 -2
  246. package/dist/survival/decayMeter.d.ts +1 -1
  247. package/dist/survival/decayMeter.js +0 -1
  248. package/dist/tactics/fallingGrid.d.ts +62 -0
  249. package/dist/tactics/fallingGrid.js +158 -0
  250. package/dist/tactics/tacticalGrid.d.ts +11 -0
  251. package/dist/tactics/tacticalGrid.js +17 -0
  252. package/dist/time/beatClock.d.ts +18 -0
  253. package/dist/time/beatClock.js +24 -0
  254. package/dist/time/idleProgress.d.ts +29 -0
  255. package/dist/time/idleProgress.js +31 -0
  256. package/dist/time/serverTick.d.ts +7 -1
  257. package/dist/time/serverTick.js +27 -4
  258. package/dist/time/simClock.d.ts +25 -1
  259. package/dist/time/simClock.js +28 -3
  260. package/dist/time/stateSchedule.d.ts +57 -0
  261. package/dist/time/stateSchedule.js +121 -0
  262. package/dist/turn/commit.d.ts +2 -2
  263. package/dist/turn/commit.js +2 -2
  264. package/dist/turn/intent.d.ts +14 -0
  265. package/dist/turn/intent.js +29 -0
  266. package/dist/turn/turnLoop.d.ts +2 -0
  267. package/dist/turn/turnLoop.js +8 -1
  268. package/dist/ui/hudLayout.d.ts +58 -0
  269. package/dist/ui/hudLayout.js +207 -0
  270. package/dist/ui/hudScale.d.ts +44 -0
  271. package/dist/ui/hudScale.js +55 -0
  272. package/dist/visibility/assetStreaming.d.ts +66 -0
  273. package/dist/visibility/assetStreaming.js +201 -0
  274. package/dist/visibility/bounds.d.ts +66 -0
  275. package/dist/visibility/bounds.js +113 -0
  276. package/dist/visibility/camera.d.ts +21 -0
  277. package/dist/visibility/camera.js +3 -0
  278. package/dist/visibility/config.d.ts +21 -0
  279. package/dist/visibility/config.js +1 -0
  280. package/dist/visibility/diagnostics.d.ts +26 -0
  281. package/dist/visibility/diagnostics.js +36 -0
  282. package/dist/visibility/distance.d.ts +11 -0
  283. package/dist/visibility/distance.js +30 -0
  284. package/dist/visibility/frustum.d.ts +47 -0
  285. package/dist/visibility/frustum.js +186 -0
  286. package/dist/visibility/occlusion.d.ts +36 -0
  287. package/dist/visibility/occlusion.js +45 -0
  288. package/dist/visibility/settings.d.ts +75 -0
  289. package/dist/visibility/settings.js +49 -0
  290. package/dist/visibility/simulationCulling.d.ts +34 -0
  291. package/dist/visibility/simulationCulling.js +36 -0
  292. package/dist/visibility/spatialIndex.d.ts +45 -0
  293. package/dist/visibility/spatialIndex.js +198 -0
  294. package/dist/visibility/visibilitySystem.d.ts +90 -0
  295. package/dist/visibility/visibilitySystem.js +257 -0
  296. package/dist/world/buildings.d.ts +6 -0
  297. package/dist/world/buildings.js +152 -0
  298. package/dist/world/carve.d.ts +7 -2
  299. package/dist/world/carve.js +17 -7
  300. package/dist/world/cellStates.d.ts +45 -0
  301. package/dist/world/cellStates.js +103 -0
  302. package/dist/world/environmentSummary.d.ts +61 -0
  303. package/dist/world/environmentSummary.js +138 -0
  304. package/dist/world/features.d.ts +138 -9
  305. package/dist/world/features.js +73 -2
  306. package/dist/world/gridInstances.d.ts +12 -0
  307. package/dist/world/gridInstances.js +160 -0
  308. package/dist/world/lod.d.ts +17 -0
  309. package/dist/world/lod.js +66 -0
  310. package/dist/world/mapLayers.d.ts +60 -0
  311. package/dist/world/mapLayers.js +44 -0
  312. package/dist/world/massing.d.ts +45 -0
  313. package/dist/world/massing.js +581 -0
  314. package/dist/world/minimap.d.ts +13 -1
  315. package/dist/world/minimap.js +21 -2
  316. package/dist/world/placementController.d.ts +22 -1
  317. package/dist/world/placementController.js +56 -0
  318. package/dist/world/terrain.d.ts +62 -1
  319. package/dist/world/terrain.js +253 -9
  320. package/dist/world/voxelField.d.ts +54 -0
  321. package/dist/world/voxelField.js +241 -0
  322. package/dist/world/water.d.ts +7 -0
  323. package/dist/world/water.js +19 -4
  324. package/dist/world/weather.d.ts +3 -18
  325. package/dist/world/weather.js +4 -17
  326. package/dist/world/windZones.d.ts +47 -0
  327. package/dist/world/windZones.js +59 -0
  328. package/llms.txt +4983 -0
  329. package/package.json +14 -3
@@ -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) {
@@ -62,6 +62,27 @@ export declare function drainPendingLeaderboardIncrements(session: Record<string
62
62
  increments: LeaderboardIncrement[];
63
63
  session: Record<string, unknown>;
64
64
  };
65
+ export declare const LEADERBOARD_TOP_LIMIT = 100;
66
+ export declare function clampLimit(value: number | undefined, fallback: number, max: number): number;
67
+ export type LeaderboardRow = {
68
+ gameId: string;
69
+ stat: string;
70
+ scope: LeaderboardScope;
71
+ serverId?: string;
72
+ userId: string;
73
+ value: number;
74
+ updatedAt: number;
75
+ };
76
+ export declare function leaderboardRowKey(row: Omit<LeaderboardRow, "value" | "updatedAt">): string;
77
+ export declare function applyLeaderboardRows(rows: Map<string, LeaderboardRow>, gameId: string, entries: LeaderboardIncrement[], now: number): void;
78
+ export declare function topLeaderboardRows(rows: Iterable<LeaderboardRow>, args: {
79
+ gameId: string;
80
+ stat: string;
81
+ scope: LeaderboardScope;
82
+ serverId?: string;
83
+ limit?: number;
84
+ }): LeaderboardEntry[];
85
+ export declare function profileLeaderboardStats(rows: Iterable<LeaderboardRow>, gameId: string, userId: string): Record<string, number>;
65
86
  export declare const FEED_RING_LIMIT = 20;
66
87
  export declare function trimFeedEntries<T>(entries: T[], limit?: number): T[];
67
88
  export declare function shouldAutoSave(save: SaveConfig, dirtyAt: number | undefined, lastSavedAt: number | undefined, now: number): boolean;
@@ -77,7 +98,13 @@ export type ServerListing = {
77
98
  tags?: string[];
78
99
  updatedAt: number;
79
100
  };
80
- 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;
107
+ export declare function toOpenServerListings(listings: Iterable<ServerListing>, limit?: number): ServerListing[];
81
108
  export type HostPersistence = {
82
109
  savePlan?: (plan: ServerPersistPlan) => Promise<void>;
83
110
  resetScenario?: (reset: NormalizedScenarioReset) => Promise<void>;
@@ -91,6 +118,7 @@ export type HostPersistence = {
91
118
  saveProfile: (record: PlayerProfileRecord) => Promise<void>;
92
119
  loadChunks: (serverId: string) => Promise<WorldChunkRecord[]>;
93
120
  saveChunks: (serverId: string, chunks: WorldChunkRecord[]) => Promise<void>;
121
+ deleteChunks: (serverId: string, chunkKeys: string[]) => Promise<void>;
94
122
  loadFeed: (args: {
95
123
  serverId: string;
96
124
  action: string;
@@ -118,6 +146,7 @@ export type ServerPersistPlan = {
118
146
  server: GameServerRecord;
119
147
  profiles: PlayerProfileRecord[];
120
148
  chunks: WorldChunkRecord[];
149
+ deletedChunks: string[];
121
150
  leaderboard: LeaderboardIncrement[];
122
151
  };
123
152
  export declare function planServerPersist(server: GameServerRecord, snapshot: GameRuntimeSnapshot, save: SaveConfig, now: number): ServerPersistPlan;
@@ -20,6 +20,56 @@ export function drainPendingLeaderboardIncrements(session) {
20
20
  delete rest[LEADERBOARD_PENDING_KEY];
21
21
  return { increments: raw.filter(isLeaderboardIncrement), session: rest };
22
22
  }
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
+ }
30
+ export function leaderboardRowKey(row) {
31
+ return [row.gameId, row.scope, row.stat, row.serverId ?? "", row.userId].join("|");
32
+ }
33
+ export function applyLeaderboardRows(rows, gameId, entries, now) {
34
+ for (const entry of entries) {
35
+ const key = leaderboardRowKey({ ...entry, gameId });
36
+ const existing = rows.get(key);
37
+ if (existing) {
38
+ rows.set(key, { ...existing, value: existing.value + entry.by, updatedAt: now });
39
+ }
40
+ else {
41
+ rows.set(key, {
42
+ gameId,
43
+ stat: entry.stat,
44
+ scope: entry.scope,
45
+ serverId: entry.serverId,
46
+ userId: entry.userId,
47
+ value: entry.by,
48
+ updatedAt: now,
49
+ });
50
+ }
51
+ }
52
+ }
53
+ export function topLeaderboardRows(rows, args) {
54
+ const limit = clampLimit(args.limit, LEADERBOARD_TOP_LIMIT, LEADERBOARD_TOP_LIMIT);
55
+ return [...rows]
56
+ .filter((row) => row.gameId === args.gameId &&
57
+ row.stat === args.stat &&
58
+ row.scope === args.scope &&
59
+ (args.serverId === undefined || row.serverId === args.serverId))
60
+ .sort((a, b) => b.value - a.value)
61
+ .slice(0, limit)
62
+ .map((row) => ({ userId: row.userId, value: row.value }));
63
+ }
64
+ export function profileLeaderboardStats(rows, gameId, userId) {
65
+ const profile = {};
66
+ for (const row of rows) {
67
+ if (row.gameId === gameId && row.userId === userId && row.scope === "profile") {
68
+ profile[row.stat] = row.value;
69
+ }
70
+ }
71
+ return profile;
72
+ }
23
73
  export const FEED_RING_LIMIT = 20;
24
74
  export function trimFeedEntries(entries, limit = FEED_RING_LIMIT) {
25
75
  if (entries.length <= limit)
@@ -35,7 +85,7 @@ export function shouldAutoSave(save, dirtyAt, lastSavedAt, now) {
35
85
  const anchor = lastSavedAt ?? 0;
36
86
  return now - anchor >= intervalMs;
37
87
  }
38
- export function toServerListing(record) {
88
+ export function toServerListing(record, options = {}) {
39
89
  return {
40
90
  serverId: record.serverId,
41
91
  status: record.status,
@@ -44,11 +94,21 @@ export function toServerListing(record) {
44
94
  mode: record.mode,
45
95
  label: record.label,
46
96
  visibility: record.visibility,
47
- joinCode: record.joinCode,
97
+ ...(options.includeJoinCode === true && record.joinCode !== undefined
98
+ ? { joinCode: record.joinCode }
99
+ : {}),
48
100
  tags: record.tags,
49
101
  updatedAt: record.updatedAt,
50
102
  };
51
103
  }
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) {
107
+ return [...listings]
108
+ .filter((listing) => listing.status === "running")
109
+ .sort((a, b) => b.updatedAt - a.updatedAt)
110
+ .slice(0, clampLimit(limit, OPEN_SERVER_LISTING_LIMIT, OPEN_SERVER_LISTING_MAX));
111
+ }
52
112
  export function buildHydratePlayers(server, profiles) {
53
113
  const players = {};
54
114
  for (const userId of server.memberUserIds) {
@@ -96,12 +156,16 @@ export function planServerPersist(server, snapshot, save, now) {
96
156
  }
97
157
  }
98
158
  const chunks = [];
159
+ const deletedChunks = [];
99
160
  if (isSaveEnabled(save) && saveScopeIncludesChunks(save.scope)) {
100
161
  for (const chunkKey of snapshot.dirty.chunks) {
101
162
  const chunk = snapshot.chunks[chunkKey];
102
- if (!chunk)
103
- continue;
104
- 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
+ }
105
169
  }
106
170
  }
107
171
  return {
@@ -116,6 +180,7 @@ export function planServerPersist(server, snapshot, save, now) {
116
180
  },
117
181
  profiles,
118
182
  chunks,
183
+ deletedChunks,
119
184
  leaderboard,
120
185
  };
121
186
  }
@@ -0,0 +1,12 @@
1
+ import type { PointerAxisState } from "../input/pointerAxis.js";
2
+ export interface InputSnapshot {
3
+ /** 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. */
4
+ publish(held: readonly string[]): void;
5
+ /** Replaces the normalized pointer-position state for this frame (#293). Same no-notify contract as `publish`. */
6
+ publishPointer(state: PointerAxisState | null): void;
7
+ isDown(action: string): boolean;
8
+ held(): readonly string[];
9
+ /** 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. */
10
+ pointer(): PointerAxisState | null;
11
+ }
12
+ export declare function createInputSnapshot(): InputSnapshot;
@@ -0,0 +1,17 @@
1
+ export function createInputSnapshot() {
2
+ let heldSet = new Set();
3
+ let heldList = [];
4
+ let pointerState = null;
5
+ return {
6
+ publish(held) {
7
+ heldList = held;
8
+ heldSet = new Set(held);
9
+ },
10
+ publishPointer(state) {
11
+ pointerState = state;
12
+ },
13
+ isDown: (action) => heldSet.has(action),
14
+ held: () => heldList,
15
+ pointer: () => pointerState,
16
+ };
17
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Seam for game code to reach the motion the shell's FrameDriver
3
+ * otherwise owns privately (#162.4). Game code calls `impulse`,
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.
12
+ *
13
+ * Intentionally not `ctx.version()`/`ctx.subscribe` wired: these fire at
14
+ * most once per frame and would otherwise storm subscribers with per-frame
15
+ * churn (same rationale as `InputSnapshot.publish`).
16
+ */
17
+ export interface MotionIntents {
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;
21
+ setVerticalVelocity(velocityY: number): void;
22
+ setY(y: number): void;
23
+ takePending(): MotionIntentBatch | null;
24
+ }
25
+ export interface MotionIntentBatch {
26
+ impulses: readonly number[];
27
+ horizontalImpulses: readonly (readonly [number, number])[];
28
+ verticalVelocity: number | null;
29
+ y: number | null;
30
+ }
31
+ export declare function createMotionIntents(): MotionIntents;
32
+ /** Fold a batch's horizontal pushes into a controller's velocity pair — shared by the walk and voxel drivers. */
33
+ export declare function applyHorizontalImpulses(velocityX: number, velocityZ: number, batch: MotionIntentBatch | null): readonly [number, number];
@@ -0,0 +1,43 @@
1
+ export function createMotionIntents() {
2
+ let impulses = [];
3
+ let horizontalImpulses = [];
4
+ let verticalVelocity = null;
5
+ let y = null;
6
+ return {
7
+ impulse(velocityY) {
8
+ impulses.push(velocityY);
9
+ },
10
+ pushHorizontal(velocityX, velocityZ) {
11
+ horizontalImpulses.push([velocityX, velocityZ]);
12
+ },
13
+ setVerticalVelocity(velocityY) {
14
+ verticalVelocity = velocityY;
15
+ },
16
+ setY(nextY) {
17
+ y = nextY;
18
+ },
19
+ takePending() {
20
+ if (impulses.length === 0 && horizontalImpulses.length === 0 && verticalVelocity === null && y === null) {
21
+ return null;
22
+ }
23
+ const batch = { impulses, horizontalImpulses, verticalVelocity, y };
24
+ impulses = [];
25
+ horizontalImpulses = [];
26
+ verticalVelocity = null;
27
+ y = null;
28
+ return batch;
29
+ },
30
+ };
31
+ }
32
+ /** Fold a batch's horizontal pushes into a controller's velocity pair — shared by the walk and voxel drivers. */
33
+ export function applyHorizontalImpulses(velocityX, velocityZ, batch) {
34
+ if (batch === null || batch.horizontalImpulses.length === 0)
35
+ return [velocityX, velocityZ];
36
+ let x = velocityX;
37
+ let z = velocityZ;
38
+ for (const [ix, iz] of batch.horizontalImpulses) {
39
+ x += ix;
40
+ z += iz;
41
+ }
42
+ return [x, z];
43
+ }
@@ -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: [],
@@ -1,3 +1,5 @@
1
+ import type { ChatSync } from "../multiplayer/chatContract.js";
2
+ import type { PlayerPose } from "../multiplayer/poseSyncGate.js";
1
3
  import type { PresenceTransport } from "../multiplayer/presenceContract.js";
2
4
  export type JoinServerResult = {
3
5
  serverId: string;
@@ -58,3 +60,33 @@ export type GameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGa
58
60
  feeds?: GameRuntimeFeeds;
59
61
  presence?: PresenceTransport<TPresenceRow, TPresenceLocation, TGameId>;
60
62
  };
63
+ export type PresencePoseRow = {
64
+ userId: string;
65
+ position: {
66
+ x: number;
67
+ y: number;
68
+ z: number;
69
+ };
70
+ rotationY: number;
71
+ rotationPitch: number;
72
+ lastSeenAt: number;
73
+ };
74
+ export type PresenceSync = {
75
+ subscribe: (serverId: string, onChange: (rows: PresencePoseRow[]) => void) => FeedUnsubscribe;
76
+ syncPose: (serverId: string, pose: PlayerPose) => void;
77
+ };
78
+ export type LiveGameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = GameBackend<TPresenceRow, TPresenceLocation, TGameId> & {
79
+ presenceSync: PresenceSync;
80
+ pushFeedEntry: (args: {
81
+ serverId: string;
82
+ action: string;
83
+ entry: unknown;
84
+ }) => Promise<void>;
85
+ chatSyncFor?: (serverId: string) => ChatSync;
86
+ };
87
+ export type MultiplayerSession = {
88
+ gameId: string;
89
+ userId: string;
90
+ backend: LiveGameBackend;
91
+ feedActions: string[];
92
+ };
@@ -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
+ }
@@ -0,0 +1,127 @@
1
+ /** Default `VisibilityDefaults` a `VisibilitySystem` falls back to when unspecified. */
2
+ export const DEFAULT_VISIBILITY_SETTINGS = Object.freeze({
3
+ enabled: true,
4
+ streamingEnabled: true,
5
+ minRenderDistance: 0,
6
+ maxRenderDistance: Number.POSITIVE_INFINITY,
7
+ preloadMargin: 24,
8
+ unloadGraceMs: 5_000,
9
+ hysteresis: 8,
10
+ });
11
+ function squaredDistance(a, b) {
12
+ const dx = a.x - b.x;
13
+ const dy = a.y - b.y;
14
+ const dz = (a.z ?? 0) - (b.z ?? 0);
15
+ return dx * dx + dy * dy + dz * dz;
16
+ }
17
+ function intersectsRadius(camera, bounds, extraMargin) {
18
+ const radius = Math.max(0, camera.visibleRadius + extraMargin + bounds.radius);
19
+ return squaredDistance(camera.position, bounds.center) <= radius * radius;
20
+ }
21
+ /**
22
+ * Engine-level visibility and asset-residency policy.
23
+ *
24
+ * Render culling and asset streaming are intentionally separate. This class never
25
+ * destroys entities or throttles simulation; adapters decide how to apply results.
26
+ */
27
+ export class VisibilitySystem {
28
+ settings;
29
+ #states = new Map();
30
+ constructor(settings = {}) {
31
+ this.settings = { ...DEFAULT_VISIBILITY_SETTINGS, ...settings };
32
+ }
33
+ evaluate(object, cameras, now = Date.now()) {
34
+ const overrides = object.overrides ?? {};
35
+ const state = this.#states.get(object.id) ?? { loaded: false, lastNeededAt: now };
36
+ if (overrides.alwaysVisible) {
37
+ this.#markNeeded(object.id, state, now);
38
+ return { render: true, load: true, unload: false, reason: "always-visible" };
39
+ }
40
+ if (!this.settings.enabled || overrides.cullingDisabled) {
41
+ this.#markNeeded(object.id, state, now);
42
+ return { render: true, load: true, unload: false, reason: "culling-disabled" };
43
+ }
44
+ const minDistance = Math.max(0, overrides.minRenderDistance ?? this.settings.minRenderDistance);
45
+ const maxDistance = Math.max(minDistance, overrides.maxRenderDistance ?? this.settings.maxRenderDistance);
46
+ const minDistanceSq = minDistance * minDistance;
47
+ const maxDistanceWithRadius = maxDistance + object.bounds.radius;
48
+ const maxDistanceSq = maxDistanceWithRadius * maxDistanceWithRadius;
49
+ let insideDistance = false;
50
+ let visible = false;
51
+ let preload = false;
52
+ for (const camera of cameras) {
53
+ const distanceSq = squaredDistance(camera.position, object.bounds.center);
54
+ if (distanceSq < minDistanceSq || distanceSq > maxDistanceSq)
55
+ continue;
56
+ insideDistance = true;
57
+ if (intersectsRadius(camera, object.bounds, this.settings.hysteresis)) {
58
+ visible = true;
59
+ }
60
+ if (camera.influencesStreaming !== false) {
61
+ const margin = overrides.preloadMargin ?? camera.preloadMargin ?? this.settings.preloadMargin;
62
+ if (intersectsRadius(camera, object.bounds, margin + this.settings.hysteresis)) {
63
+ preload = true;
64
+ }
65
+ }
66
+ }
67
+ if (visible) {
68
+ this.#markNeeded(object.id, state, now);
69
+ return { render: true, load: true, unload: false, reason: "visible" };
70
+ }
71
+ if (overrides.streamingDisabled || !this.settings.streamingEnabled) {
72
+ state.loaded = true;
73
+ this.#states.set(object.id, state);
74
+ return {
75
+ render: false,
76
+ load: true,
77
+ unload: false,
78
+ reason: "streaming-disabled",
79
+ };
80
+ }
81
+ if (preload) {
82
+ this.#markNeeded(object.id, state, now);
83
+ return { render: false, load: true, unload: false, reason: "preload" };
84
+ }
85
+ if (overrides.neverUnload) {
86
+ state.loaded = true;
87
+ this.#states.set(object.id, state);
88
+ return {
89
+ render: false,
90
+ load: true,
91
+ unload: false,
92
+ reason: insideDistance ? "outside-view" : "distance-culled",
93
+ };
94
+ }
95
+ const graceActive = state.loaded && now - state.lastNeededAt < this.settings.unloadGraceMs;
96
+ if (graceActive) {
97
+ this.#states.set(object.id, state);
98
+ return { render: false, load: true, unload: false, reason: "grace-period" };
99
+ }
100
+ state.loaded = false;
101
+ this.#states.set(object.id, state);
102
+ return {
103
+ render: false,
104
+ load: false,
105
+ unload: true,
106
+ reason: insideDistance ? "outside-view" : "distance-culled",
107
+ };
108
+ }
109
+ setLoaded(id, loaded, now = Date.now()) {
110
+ const previous = this.#states.get(id);
111
+ this.#states.set(id, {
112
+ loaded,
113
+ lastNeededAt: loaded ? now : previous?.lastNeededAt ?? now,
114
+ });
115
+ }
116
+ forget(id) {
117
+ this.#states.delete(id);
118
+ }
119
+ clear() {
120
+ this.#states.clear();
121
+ }
122
+ #markNeeded(id, state, now) {
123
+ state.loaded = true;
124
+ state.lastNeededAt = now;
125
+ this.#states.set(id, state);
126
+ }
127
+ }
@@ -0,0 +1,44 @@
1
+ import type { EntityPosition } from "./entityStore.js";
2
+ export type ColliderPurpose = "physical" | "damage";
3
+ export type ColliderShape = {
4
+ kind: "sphere";
5
+ radius: number;
6
+ offset?: EntityPosition;
7
+ } | {
8
+ kind: "aabb";
9
+ halfExtents: EntityPosition;
10
+ offset?: EntityPosition;
11
+ };
12
+ export interface ColliderDef {
13
+ name: string;
14
+ purpose: ColliderPurpose;
15
+ shape: ColliderShape;
16
+ /** Defaults true for `purpose: "damage"`, false for `purpose: "physical"`. */
17
+ damageEligible?: boolean;
18
+ /** Defaults true — a blocking collider stops a ray at first impact. */
19
+ blocks?: boolean;
20
+ }
21
+ export interface EntityColliderSet {
22
+ /** Single physical collision body (push / obstruction). */
23
+ body?: ColliderDef;
24
+ /** Named damage hitboxes (zero or more). */
25
+ hitboxes?: readonly ColliderDef[];
26
+ }
27
+ export interface ResolvedCollider {
28
+ name: string;
29
+ purpose: ColliderPurpose;
30
+ shape: ColliderShape;
31
+ damageEligible: boolean;
32
+ blocks: boolean;
33
+ }
34
+ export declare const DEFAULT_ENTITY_HIT_RADIUS = 0.5;
35
+ export declare const DEFAULT_OBJECT_HALF_EXTENTS: EntityPosition;
36
+ export declare function defaultEntityColliders(): EntityColliderSet;
37
+ export declare function defaultObjectColliders(halfExtents?: EntityPosition): EntityColliderSet;
38
+ export declare function resolveColliders(set: EntityColliderSet | null | undefined): ResolvedCollider[];
39
+ export declare function worldOffset(local: EntityPosition | undefined, position: EntityPosition, rotationY: number): EntityPosition;
40
+ export declare function colliderWorldCenter(collider: ResolvedCollider, position: EntityPosition, rotationY: number): EntityPosition;
41
+ export declare function colliderBounds(collider: ResolvedCollider, position: EntityPosition, rotationY: number): {
42
+ min: EntityPosition;
43
+ max: EntityPosition;
44
+ };