1681-engine 0.5.8 → 0.5.10

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 (724) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +3 -0
  3. package/dist/1681-engine.js +526 -409
  4. package/dist/ambient/ColorGrade.js +0 -1
  5. package/dist/ambient/DayClock.js +0 -1
  6. package/dist/ambient/WeatherLayer.js +0 -1
  7. package/dist/ambient/grade.js +0 -1
  8. package/dist/ambient/life/Birds.js +73 -0
  9. package/dist/ambient/life/Butterflies.js +43 -0
  10. package/dist/ambient/life/ChimneySmoke.js +50 -0
  11. package/dist/ambient/life/Drift.js +75 -0
  12. package/dist/ambient/life/Fireflies.js +52 -0
  13. package/dist/ambient/life/lifeMotion.js +49 -0
  14. package/dist/ambient/weather.js +0 -1
  15. package/dist/anim/AnimStateMachine.js +0 -1
  16. package/dist/anim/Animator.js +0 -1
  17. package/dist/anim/animStateCompile.js +0 -1
  18. package/dist/anim/animStateSystem.js +0 -1
  19. package/dist/anim/animationSystem.js +0 -1
  20. package/dist/anim/asepriteSlices.js +0 -1
  21. package/dist/anim/asepriteTags.js +0 -1
  22. package/dist/anim/parseAseprite.js +0 -1
  23. package/dist/assets/AssetLoader.js +177 -34
  24. package/dist/assets/Prepainter.js +61 -0
  25. package/dist/assets/loadAseprite.js +0 -1
  26. package/dist/assets/takeCleanerWorker-DTcI_Jqr.js +0 -1
  27. package/dist/assets/textureAtlas.js +58 -0
  28. package/dist/assets/workerPainter.js +39 -0
  29. package/dist/audio/Footsteps.js +0 -1
  30. package/dist/audio/MusicPlayer.js +0 -1
  31. package/dist/audio/SoundEngine.js +13 -7
  32. package/dist/audio/cleanTake.js +0 -1
  33. package/dist/audio/music/ElementVoice.js +0 -1
  34. package/dist/audio/music/GraphVoice.js +0 -1
  35. package/dist/audio/music/MusicChannel.js +0 -1
  36. package/dist/audio/music/musicVoice.js +0 -1
  37. package/dist/audio/sampleBank.js +0 -1
  38. package/dist/audio/soundTable.js +0 -1
  39. package/dist/audio/synth.js +0 -1
  40. package/dist/cjs/electron/electronLike.d.ts +55 -0
  41. package/dist/cjs/electron/electronLike.js +7 -0
  42. package/dist/cjs/electron/index.d.ts +14 -0
  43. package/dist/cjs/electron/index.js +20 -0
  44. package/dist/cjs/electron/lockdown.d.ts +33 -0
  45. package/dist/cjs/electron/lockdown.js +69 -0
  46. package/dist/cjs/electron/lockdownRules.d.ts +28 -0
  47. package/dist/cjs/electron/lockdownRules.js +61 -0
  48. package/dist/cjs/electron/sessionStore.d.ts +40 -0
  49. package/dist/cjs/electron/sessionStore.js +77 -0
  50. package/dist/cjs/electron/shellId.d.ts +22 -0
  51. package/dist/cjs/electron/shellId.js +31 -0
  52. package/dist/cjs/electron/webFiles.d.ts +13 -0
  53. package/dist/cjs/electron/webFiles.js +52 -0
  54. package/dist/cjs/electron/webPatcher.d.ts +80 -0
  55. package/dist/cjs/electron/webPatcher.js +216 -0
  56. package/dist/cjs/electron/webPointers.d.ts +19 -0
  57. package/dist/cjs/electron/webPointers.js +35 -0
  58. package/dist/cjs/package.json +1 -0
  59. package/dist/cjs/updates/index.d.ts +8 -0
  60. package/dist/cjs/updates/index.js +21 -0
  61. package/dist/cjs/updates/manifest.d.ts +73 -0
  62. package/dist/cjs/updates/manifest.js +67 -0
  63. package/dist/cjs/updates/pickUpdate.d.ts +42 -0
  64. package/dist/cjs/updates/pickUpdate.js +52 -0
  65. package/dist/cjs/updates/webPlan.d.ts +20 -0
  66. package/dist/cjs/updates/webPlan.js +34 -0
  67. package/dist/core/BitmapLabel.js +0 -1
  68. package/dist/core/Camera.js +1 -2
  69. package/dist/core/Clock.js +31 -21
  70. package/dist/core/Graphics.js +0 -1
  71. package/dist/core/Group.js +0 -1
  72. package/dist/core/Image.js +0 -1
  73. package/dist/core/ObjectFactory.js +0 -1
  74. package/dist/core/Pointer.js +46 -24
  75. package/dist/core/Rectangle.js +0 -1
  76. package/dist/core/Stage.js +5 -6
  77. package/dist/core/Text.js +0 -1
  78. package/dist/core/TextureRegistry.js +54 -24
  79. package/dist/core/Timeline.js +0 -1
  80. package/dist/core/TimelineBuilder.js +0 -1
  81. package/dist/core/TimelineChild.js +0 -1
  82. package/dist/core/Tweener.js +0 -1
  83. package/dist/core/atlas.js +0 -1
  84. package/dist/core/atlasPlan.js +0 -1
  85. package/dist/core/blendLayer.js +11 -0
  86. package/dist/core/eases.js +0 -1
  87. package/dist/core/gameObject.js +22 -20
  88. package/dist/core/pixelZoom.js +43 -0
  89. package/dist/core/pixiPatches.js +0 -1
  90. package/dist/core/runDue.js +28 -0
  91. package/dist/core/silhouette.js +49 -0
  92. package/dist/core/timelinePlace.js +0 -1
  93. package/dist/core/tweenProps.js +0 -1
  94. package/dist/debug/DebugOverlay.js +0 -1
  95. package/dist/debug/DrawCallCounter.js +15 -12
  96. package/dist/debug/ErrorLog.js +0 -1
  97. package/dist/debug/FrameStats.js +0 -1
  98. package/dist/debug/inspectWorld.js +0 -1
  99. package/dist/diagnostics/ErrorDeduper.js +0 -1
  100. package/dist/diagnostics/FrameSampler.js +0 -1
  101. package/dist/diagnostics/bootWatch.js +0 -1
  102. package/dist/diagnostics/captureErrors.js +0 -1
  103. package/dist/diagnostics/contextLoss.js +0 -1
  104. package/dist/diagnostics/frameStats.js +0 -1
  105. package/dist/diagnostics/gpuInfo.js +0 -1
  106. package/dist/diagnostics/renderGuard.js +0 -1
  107. package/dist/ecs/SparseSet.js +0 -1
  108. package/dist/ecs/World.js +0 -1
  109. package/dist/ecs/fields.js +0 -1
  110. package/dist/electron/lockdown.js +31 -0
  111. package/dist/electron/lockdownRules.js +42 -0
  112. package/dist/electron/sessionStore.js +48 -0
  113. package/dist/electron/shellId.js +18 -0
  114. package/dist/electron/webFiles.js +36 -0
  115. package/dist/electron/webPatcher.js +147 -0
  116. package/dist/electron/webPointers.js +25 -0
  117. package/dist/electron.js +19 -0
  118. package/dist/engine/Engine.js +42 -18
  119. package/dist/engine/FixedLoop.js +0 -1
  120. package/dist/engine/HiddenPause.js +27 -0
  121. package/dist/engine/Scene.js +0 -1
  122. package/dist/engine/SceneManager.js +0 -1
  123. package/dist/events/EventBus.js +0 -1
  124. package/dist/fx/Afterimages.js +82 -0
  125. package/dist/fx/FloatingText.js +0 -1
  126. package/dist/fx/hitStop.js +0 -1
  127. package/dist/fx/viewCull.js +0 -1
  128. package/dist/i18n/Translator.js +0 -1
  129. package/dist/i18n/extractStrings.js +0 -1
  130. package/dist/i18n/formatMessage.js +0 -1
  131. package/dist/input/Actions.js +29 -21
  132. package/dist/input/AimGesture.js +0 -1
  133. package/dist/input/Keyboard.js +0 -1
  134. package/dist/input/Keymap.js +0 -1
  135. package/dist/input/TargetLock.js +0 -1
  136. package/dist/input/aimAxis.js +0 -1
  137. package/dist/input/haptics.js +50 -0
  138. package/dist/input/keyChord.js +0 -1
  139. package/dist/input/longPress.js +0 -1
  140. package/dist/input/rumble.js +42 -0
  141. package/dist/input/stickAxis.js +0 -1
  142. package/dist/input/touchDetect.js +0 -1
  143. package/dist/light/LightLayer.js +0 -1
  144. package/dist/nav/MinHeap.js +0 -1
  145. package/dist/nav/findPath.js +0 -1
  146. package/dist/nav/navGrid.js +0 -1
  147. package/dist/nav/nearestOpen.js +0 -1
  148. package/dist/nav/straightenPath.js +0 -1
  149. package/dist/net/ApiClient.js +96 -0
  150. package/dist/net/Connection.js +39 -16
  151. package/dist/net/InterpolationBuffer.js +0 -1
  152. package/dist/net/PredictedMover.js +0 -1
  153. package/dist/net/Room.js +0 -1
  154. package/dist/net/RoomHost.js +0 -1
  155. package/dist/net/cadence.js +0 -1
  156. package/dist/net/constants.js +0 -1
  157. package/dist/net/lobby/LobbyClient.js +0 -1
  158. package/dist/net/lobby/LobbyHost.js +0 -1
  159. package/dist/net/lobby/LobbyRooms.js +0 -1
  160. package/dist/net/lobby/Matchmaker.js +0 -1
  161. package/dist/net/movement.js +40 -39
  162. package/dist/net/protocol.js +0 -1
  163. package/dist/net/reconnect.js +10 -0
  164. package/dist/net/serverClock.js +0 -1
  165. package/dist/net/snapshotCodec.js +0 -1
  166. package/dist/parallax/Parallax.js +0 -1
  167. package/dist/parallax/ParallaxLayer.js +0 -1
  168. package/dist/parallax/parallaxMath.js +0 -1
  169. package/dist/particles/Emitter.js +0 -1
  170. package/dist/particles/ParticleBudget.js +0 -1
  171. package/dist/particles/ParticleSim.js +0 -1
  172. package/dist/particles/particleCurves.js +0 -1
  173. package/dist/particles/particleSampling.js +0 -1
  174. package/dist/physics/Body.js +0 -1
  175. package/dist/physics/BroadPhase.js +0 -1
  176. package/dist/physics/Contact.js +0 -1
  177. package/dist/physics/ContactSolver.js +0 -1
  178. package/dist/physics/PhysicsWorld.js +0 -1
  179. package/dist/physics/SpatialHash.js +0 -1
  180. package/dist/physics/collide.js +0 -1
  181. package/dist/physics/collidePolygon.js +0 -1
  182. package/dist/physics/contactReport.js +0 -1
  183. package/dist/physics/drawColliders.js +0 -1
  184. package/dist/physics/physicsSystem.js +0 -1
  185. package/dist/physics/polygon.js +0 -1
  186. package/dist/physics/raycast.js +0 -1
  187. package/dist/physics/syncToBody.js +0 -1
  188. package/dist/physics/tileBodies.js +0 -1
  189. package/dist/physics/types.js +0 -1
  190. package/dist/pixel/Darkness.js +50 -47
  191. package/dist/pixel/DitherVignette.js +0 -1
  192. package/dist/pixel/Evaporation.js +0 -1
  193. package/dist/pixel/NightShade.js +0 -1
  194. package/dist/pixel/PixelLayer.js +0 -1
  195. package/dist/pixel/PixelMarks.js +99 -0
  196. package/dist/pixel/SwayProp.js +0 -1
  197. package/dist/pixel/aimPixels.js +67 -0
  198. package/dist/pixel/lifeArt.js +83 -0
  199. package/dist/pixel/lights.js +0 -1
  200. package/dist/pixel/nineSlice.js +0 -1
  201. package/dist/pixel/paintLayer.js +0 -1
  202. package/dist/pixel/palette.js +0 -1
  203. package/dist/pixel/pixel.js +0 -1
  204. package/dist/pixel/pixelPaint.js +27 -24
  205. package/dist/pixel/pixelParticles.js +35 -0
  206. package/dist/pixel/pixelTone.js +0 -1
  207. package/dist/pixel/propKit.js +0 -1
  208. package/dist/pixel/ramp.js +0 -1
  209. package/dist/pixel/selout.js +0 -1
  210. package/dist/pixel/spritePixels.js +0 -1
  211. package/dist/pixel/swayRig.js +0 -1
  212. package/dist/pixel/vignetteMask.js +0 -1
  213. package/dist/pixel/weatherArt.js +0 -1
  214. package/dist/pixel.js +82 -71
  215. package/dist/platform/appResume.js +18 -0
  216. package/dist/platform/fullscreen.js +52 -0
  217. package/dist/platform/orientation.js +26 -0
  218. package/dist/platform/pointerLock.js +37 -0
  219. package/dist/platform/reducedMotion.js +23 -0
  220. package/dist/platform/timeoutSignal.js +14 -0
  221. package/dist/platform/touch.js +0 -1
  222. package/dist/postfx/BloomFilter.js +0 -1
  223. package/dist/postfx/ChromaticAberrationFilter.js +0 -1
  224. package/dist/postfx/ColorAdjustFilter.js +0 -1
  225. package/dist/postfx/CrtFilter.js +0 -1
  226. package/dist/postfx/PixelateFilter.js +0 -1
  227. package/dist/postfx/PostFx.js +0 -1
  228. package/dist/postfx/ShockwaveFilter.js +0 -1
  229. package/dist/postfx/VignetteFilter.js +0 -1
  230. package/dist/postfx/fxAnimator.js +0 -1
  231. package/dist/postfx/fxShader.js +0 -1
  232. package/dist/postfx.js +0 -1
  233. package/dist/react/ChoiceDialog.js +0 -1
  234. package/dist/react/ConfirmButton.js +24 -0
  235. package/dist/react/ConfirmDialog.js +0 -1
  236. package/dist/react/Glyph.js +0 -1
  237. package/dist/react/Modal.js +0 -1
  238. package/dist/react/Panel.js +0 -1
  239. package/dist/react/PixelImage.js +0 -1
  240. package/dist/react/Select.js +0 -1
  241. package/dist/react/Slider.js +0 -1
  242. package/dist/react/SplitPane.js +0 -1
  243. package/dist/react/TabStrip.js +0 -1
  244. package/dist/react/TitleTips.js +0 -1
  245. package/dist/react/Tooltip.js +0 -1
  246. package/dist/react/UiProvider.js +0 -1
  247. package/dist/react/baseCss.js +1 -1
  248. package/dist/react/choiceStore.js +0 -1
  249. package/dist/react/classNames.js +0 -1
  250. package/dist/react/confirmStore.js +0 -1
  251. package/dist/react/labels.js +7 -7
  252. package/dist/react/placement.js +0 -1
  253. package/dist/react/useFillSheet.js +0 -1
  254. package/dist/react/useFitToViewport.js +0 -1
  255. package/dist/react/useJsonSelector.js +13 -0
  256. package/dist/react/usePicked.js +0 -1
  257. package/dist/react/useStore.js +0 -1
  258. package/dist/react/useViewport.js +0 -1
  259. package/dist/react.js +73 -69
  260. package/dist/rig/PosePlayer.js +0 -1
  261. package/dist/rig/RigView.js +0 -1
  262. package/dist/rig/Skeleton.js +0 -1
  263. package/dist/rig/humanoid.js +0 -1
  264. package/dist/rig/humanoid.json.js +0 -1
  265. package/dist/rig/humanoidPoses.js +0 -1
  266. package/dist/rig/poseMath.js +0 -1
  267. package/dist/rig/poses.json.js +0 -1
  268. package/dist/save/Autosaver.js +0 -1
  269. package/dist/save/MemoryBackend.js +0 -1
  270. package/dist/save/SaveStore.js +0 -1
  271. package/dist/save/WebStorageBackend.js +0 -1
  272. package/dist/save/checksum.js +0 -1
  273. package/dist/save/migrateSave.js +0 -1
  274. package/dist/settings.js +0 -1
  275. package/dist/tilemap/TilemapView.js +57 -33
  276. package/dist/tilemap/autotile.js +103 -0
  277. package/dist/tilemap/collision.js +29 -19
  278. package/dist/tilemap/ldtk.js +21 -0
  279. package/dist/tilemap/ldtkParse.js +191 -0
  280. package/dist/tilemap/tiled.js +0 -1
  281. package/dist/transitions/CrossfadeTransition.js +0 -1
  282. package/dist/transitions/DitherWipeTransition.js +0 -1
  283. package/dist/transitions/FadeTransition.js +0 -1
  284. package/dist/transitions/Letterbox.js +0 -1
  285. package/dist/transitions/ditherMask.js +0 -1
  286. package/dist/transitions/transitionTo.js +0 -1
  287. package/dist/transitions/tweenDone.js +0 -1
  288. package/dist/types/ambient/life/Birds.d.ts +50 -0
  289. package/dist/types/ambient/life/Birds.d.ts.map +1 -0
  290. package/dist/types/ambient/life/Butterflies.d.ts +32 -0
  291. package/dist/types/ambient/life/Butterflies.d.ts.map +1 -0
  292. package/dist/types/ambient/life/ChimneySmoke.d.ts +37 -0
  293. package/dist/types/ambient/life/ChimneySmoke.d.ts.map +1 -0
  294. package/dist/types/ambient/life/Drift.d.ts +58 -0
  295. package/dist/types/ambient/life/Drift.d.ts.map +1 -0
  296. package/dist/types/ambient/life/Fireflies.d.ts +43 -0
  297. package/dist/types/ambient/life/Fireflies.d.ts.map +1 -0
  298. package/dist/types/ambient/life/lifeMotion.d.ts +63 -0
  299. package/dist/types/ambient/life/lifeMotion.d.ts.map +1 -0
  300. package/dist/types/assets/AssetLoader.d.ts +83 -8
  301. package/dist/types/assets/AssetLoader.d.ts.map +1 -1
  302. package/dist/types/assets/Prepainter.d.ts +64 -0
  303. package/dist/types/assets/Prepainter.d.ts.map +1 -0
  304. package/dist/types/assets/textureAtlas.d.ts +35 -0
  305. package/dist/types/assets/textureAtlas.d.ts.map +1 -0
  306. package/dist/types/assets/workerPainter.d.ts +21 -0
  307. package/dist/types/assets/workerPainter.d.ts.map +1 -0
  308. package/dist/types/audio/SoundEngine.d.ts +5 -0
  309. package/dist/types/audio/SoundEngine.d.ts.map +1 -1
  310. package/dist/types/core/Camera.d.ts +1 -1
  311. package/dist/types/core/Camera.d.ts.map +1 -1
  312. package/dist/types/core/Clock.d.ts +14 -2
  313. package/dist/types/core/Clock.d.ts.map +1 -1
  314. package/dist/types/core/Pointer.d.ts +18 -0
  315. package/dist/types/core/Pointer.d.ts.map +1 -1
  316. package/dist/types/core/Stage.d.ts.map +1 -1
  317. package/dist/types/core/TextureRegistry.d.ts +39 -0
  318. package/dist/types/core/TextureRegistry.d.ts.map +1 -1
  319. package/dist/types/core/blendLayer.d.ts +23 -0
  320. package/dist/types/core/blendLayer.d.ts.map +1 -0
  321. package/dist/types/core/gameObject.d.ts.map +1 -1
  322. package/dist/types/core/pixelZoom.d.ts +45 -0
  323. package/dist/types/core/pixelZoom.d.ts.map +1 -0
  324. package/dist/types/core/runDue.d.ts +15 -0
  325. package/dist/types/core/runDue.d.ts.map +1 -0
  326. package/dist/types/core/silhouette.d.ts +16 -0
  327. package/dist/types/core/silhouette.d.ts.map +1 -0
  328. package/dist/types/debug/DrawCallCounter.d.ts.map +1 -1
  329. package/dist/types/electron/electronLike.d.ts +56 -0
  330. package/dist/types/electron/electronLike.d.ts.map +1 -0
  331. package/dist/types/electron/index.d.ts +15 -0
  332. package/dist/types/electron/index.d.ts.map +1 -0
  333. package/dist/types/electron/lockdown.d.ts +34 -0
  334. package/dist/types/electron/lockdown.d.ts.map +1 -0
  335. package/dist/types/electron/lockdownRules.d.ts +29 -0
  336. package/dist/types/electron/lockdownRules.d.ts.map +1 -0
  337. package/dist/types/electron/sessionStore.d.ts +41 -0
  338. package/dist/types/electron/sessionStore.d.ts.map +1 -0
  339. package/dist/types/electron/shellId.d.ts +23 -0
  340. package/dist/types/electron/shellId.d.ts.map +1 -0
  341. package/dist/types/electron/webFiles.d.ts +14 -0
  342. package/dist/types/electron/webFiles.d.ts.map +1 -0
  343. package/dist/types/electron/webPatcher.d.ts +81 -0
  344. package/dist/types/electron/webPatcher.d.ts.map +1 -0
  345. package/dist/types/electron/webPointers.d.ts +20 -0
  346. package/dist/types/electron/webPointers.d.ts.map +1 -0
  347. package/dist/types/engine/Engine.d.ts +16 -0
  348. package/dist/types/engine/Engine.d.ts.map +1 -1
  349. package/dist/types/engine/HiddenPause.d.ts +22 -0
  350. package/dist/types/engine/HiddenPause.d.ts.map +1 -0
  351. package/dist/types/fx/Afterimages.d.ts +62 -0
  352. package/dist/types/fx/Afterimages.d.ts.map +1 -0
  353. package/dist/types/index.d.ts +32 -4
  354. package/dist/types/index.d.ts.map +1 -1
  355. package/dist/types/input/Actions.d.ts +7 -1
  356. package/dist/types/input/Actions.d.ts.map +1 -1
  357. package/dist/types/input/haptics.d.ts +42 -0
  358. package/dist/types/input/haptics.d.ts.map +1 -0
  359. package/dist/types/input/rumble.d.ts +41 -0
  360. package/dist/types/input/rumble.d.ts.map +1 -0
  361. package/dist/types/net/ApiClient.d.ts +74 -0
  362. package/dist/types/net/ApiClient.d.ts.map +1 -0
  363. package/dist/types/net/Connection.d.ts +22 -0
  364. package/dist/types/net/Connection.d.ts.map +1 -1
  365. package/dist/types/net/constants.d.ts +13 -4
  366. package/dist/types/net/constants.d.ts.map +1 -1
  367. package/dist/types/net/movement.d.ts.map +1 -1
  368. package/dist/types/net/reconnect.d.ts +14 -0
  369. package/dist/types/net/reconnect.d.ts.map +1 -0
  370. package/dist/types/pixel/Darkness.d.ts +17 -5
  371. package/dist/types/pixel/Darkness.d.ts.map +1 -1
  372. package/dist/types/pixel/PixelMarks.d.ts +68 -0
  373. package/dist/types/pixel/PixelMarks.d.ts.map +1 -0
  374. package/dist/types/pixel/aimPixels.d.ts +23 -0
  375. package/dist/types/pixel/aimPixels.d.ts.map +1 -0
  376. package/dist/types/pixel/index.d.ts +4 -1
  377. package/dist/types/pixel/index.d.ts.map +1 -1
  378. package/dist/types/pixel/lifeArt.d.ts +63 -0
  379. package/dist/types/pixel/lifeArt.d.ts.map +1 -0
  380. package/dist/types/pixel/pixelPaint.d.ts +1 -0
  381. package/dist/types/pixel/pixelPaint.d.ts.map +1 -1
  382. package/dist/types/pixel/pixelParticles.d.ts +21 -0
  383. package/dist/types/pixel/pixelParticles.d.ts.map +1 -0
  384. package/dist/types/platform/appResume.d.ts +25 -0
  385. package/dist/types/platform/appResume.d.ts.map +1 -0
  386. package/dist/types/platform/fullscreen.d.ts +37 -0
  387. package/dist/types/platform/fullscreen.d.ts.map +1 -0
  388. package/dist/types/platform/orientation.d.ts +19 -0
  389. package/dist/types/platform/orientation.d.ts.map +1 -0
  390. package/dist/types/platform/pointerLock.d.ts +26 -0
  391. package/dist/types/platform/pointerLock.d.ts.map +1 -0
  392. package/dist/types/platform/reducedMotion.d.ts +20 -0
  393. package/dist/types/platform/reducedMotion.d.ts.map +1 -0
  394. package/dist/types/platform/timeoutSignal.d.ts +6 -0
  395. package/dist/types/platform/timeoutSignal.d.ts.map +1 -0
  396. package/dist/types/react/ConfirmButton.d.ts +26 -0
  397. package/dist/types/react/ConfirmButton.d.ts.map +1 -0
  398. package/dist/types/react/baseCss.d.ts +1 -1
  399. package/dist/types/react/baseCss.d.ts.map +1 -1
  400. package/dist/types/react/index.d.ts +2 -0
  401. package/dist/types/react/index.d.ts.map +1 -1
  402. package/dist/types/react/labels.d.ts +2 -0
  403. package/dist/types/react/labels.d.ts.map +1 -1
  404. package/dist/types/react/useJsonSelector.d.ts +13 -0
  405. package/dist/types/react/useJsonSelector.d.ts.map +1 -0
  406. package/dist/types/tilemap/TilemapView.d.ts +17 -0
  407. package/dist/types/tilemap/TilemapView.d.ts.map +1 -1
  408. package/dist/types/tilemap/autotile.d.ts +95 -0
  409. package/dist/types/tilemap/autotile.d.ts.map +1 -0
  410. package/dist/types/tilemap/collision.d.ts +8 -0
  411. package/dist/types/tilemap/collision.d.ts.map +1 -1
  412. package/dist/types/tilemap/ldtk.d.ts +97 -0
  413. package/dist/types/tilemap/ldtk.d.ts.map +1 -0
  414. package/dist/types/tilemap/ldtkParse.d.ts +26 -0
  415. package/dist/types/tilemap/ldtkParse.d.ts.map +1 -0
  416. package/dist/types/ui/index.d.ts +4 -0
  417. package/dist/types/ui/index.d.ts.map +1 -1
  418. package/dist/types/ui/rich/RichText.d.ts +48 -0
  419. package/dist/types/ui/rich/RichText.d.ts.map +1 -0
  420. package/dist/types/ui/rich/richFonts.d.ts +70 -0
  421. package/dist/types/ui/rich/richFonts.d.ts.map +1 -0
  422. package/dist/types/ui/rich/richLayout.d.ts +76 -0
  423. package/dist/types/ui/rich/richLayout.d.ts.map +1 -0
  424. package/dist/types/ui/rich/richMarkup.d.ts +48 -0
  425. package/dist/types/ui/rich/richMarkup.d.ts.map +1 -0
  426. package/dist/types/updates/index.d.ts +9 -0
  427. package/dist/types/updates/index.d.ts.map +1 -0
  428. package/dist/types/updates/manifest.d.ts +74 -0
  429. package/dist/types/updates/manifest.d.ts.map +1 -0
  430. package/dist/types/updates/pickUpdate.d.ts +43 -0
  431. package/dist/types/updates/pickUpdate.d.ts.map +1 -0
  432. package/dist/types/updates/webPlan.d.ts +21 -0
  433. package/dist/types/updates/webPlan.d.ts.map +1 -0
  434. package/dist/ui/Bar.js +0 -1
  435. package/dist/ui/Button.js +0 -1
  436. package/dist/ui/Dialog.js +0 -1
  437. package/dist/ui/FocusManager.js +0 -1
  438. package/dist/ui/Label.js +0 -1
  439. package/dist/ui/List.js +0 -1
  440. package/dist/ui/Panel.js +0 -1
  441. package/dist/ui/ScrollView.js +0 -1
  442. package/dist/ui/Slider.js +0 -1
  443. package/dist/ui/Stack.js +0 -1
  444. package/dist/ui/Toggle.js +0 -1
  445. package/dist/ui/UiHost.js +0 -1
  446. package/dist/ui/UiPointerGate.js +0 -1
  447. package/dist/ui/UiRoot.js +0 -1
  448. package/dist/ui/Widget.js +0 -1
  449. package/dist/ui/dom/DomOverlay.js +0 -1
  450. package/dist/ui/dom/DomWorldAnchors.js +0 -1
  451. package/dist/ui/dom/InputShield.js +0 -1
  452. package/dist/ui/dom/createStore.js +0 -1
  453. package/dist/ui/dom/shieldDecision.js +0 -1
  454. package/dist/ui/dom/worldToScreen.js +0 -1
  455. package/dist/ui/layout/anchor.js +0 -1
  456. package/dist/ui/layout/barDrain.js +0 -1
  457. package/dist/ui/layout/fitToViewport.js +0 -1
  458. package/dist/ui/layout/focusNav.js +0 -1
  459. package/dist/ui/layout/geometry.js +0 -1
  460. package/dist/ui/layout/listWidth.js +0 -1
  461. package/dist/ui/layout/placeTooltip.js +0 -1
  462. package/dist/ui/layout/scroll.js +0 -1
  463. package/dist/ui/layout/slider.js +0 -1
  464. package/dist/ui/layout/stack.js +0 -1
  465. package/dist/ui/layout/wrapRow.js +0 -1
  466. package/dist/ui/rich/RichText.js +87 -0
  467. package/dist/ui/rich/richFonts.js +105 -0
  468. package/dist/ui/rich/richLayout.js +132 -0
  469. package/dist/ui/rich/richMarkup.js +67 -0
  470. package/dist/ui.js +75 -65
  471. package/dist/updates/manifest.js +35 -0
  472. package/dist/updates/pickUpdate.js +27 -0
  473. package/dist/updates/webPlan.js +14 -0
  474. package/dist/updates.js +15 -0
  475. package/package.json +31 -6
  476. package/dist/1681-engine.js.map +0 -1
  477. package/dist/ambient/ColorGrade.js.map +0 -1
  478. package/dist/ambient/DayClock.js.map +0 -1
  479. package/dist/ambient/WeatherLayer.js.map +0 -1
  480. package/dist/ambient/grade.js.map +0 -1
  481. package/dist/ambient/weather.js.map +0 -1
  482. package/dist/anim/AnimStateMachine.js.map +0 -1
  483. package/dist/anim/Animator.js.map +0 -1
  484. package/dist/anim/animStateCompile.js.map +0 -1
  485. package/dist/anim/animStateSystem.js.map +0 -1
  486. package/dist/anim/animationSystem.js.map +0 -1
  487. package/dist/anim/asepriteSlices.js.map +0 -1
  488. package/dist/anim/asepriteTags.js.map +0 -1
  489. package/dist/anim/parseAseprite.js.map +0 -1
  490. package/dist/assets/AssetLoader.js.map +0 -1
  491. package/dist/assets/loadAseprite.js.map +0 -1
  492. package/dist/assets/takeCleanerWorker-DTcI_Jqr.js.map +0 -1
  493. package/dist/audio/Footsteps.js.map +0 -1
  494. package/dist/audio/MusicPlayer.js.map +0 -1
  495. package/dist/audio/SoundEngine.js.map +0 -1
  496. package/dist/audio/cleanTake.js.map +0 -1
  497. package/dist/audio/music/ElementVoice.js.map +0 -1
  498. package/dist/audio/music/GraphVoice.js.map +0 -1
  499. package/dist/audio/music/MusicChannel.js.map +0 -1
  500. package/dist/audio/music/musicVoice.js.map +0 -1
  501. package/dist/audio/sampleBank.js.map +0 -1
  502. package/dist/audio/soundTable.js.map +0 -1
  503. package/dist/audio/synth.js.map +0 -1
  504. package/dist/core/BitmapLabel.js.map +0 -1
  505. package/dist/core/Camera.js.map +0 -1
  506. package/dist/core/Clock.js.map +0 -1
  507. package/dist/core/Graphics.js.map +0 -1
  508. package/dist/core/Group.js.map +0 -1
  509. package/dist/core/Image.js.map +0 -1
  510. package/dist/core/ObjectFactory.js.map +0 -1
  511. package/dist/core/Pointer.js.map +0 -1
  512. package/dist/core/Rectangle.js.map +0 -1
  513. package/dist/core/Stage.js.map +0 -1
  514. package/dist/core/Text.js.map +0 -1
  515. package/dist/core/TextureRegistry.js.map +0 -1
  516. package/dist/core/Timeline.js.map +0 -1
  517. package/dist/core/TimelineBuilder.js.map +0 -1
  518. package/dist/core/TimelineChild.js.map +0 -1
  519. package/dist/core/Tweener.js.map +0 -1
  520. package/dist/core/atlas.js.map +0 -1
  521. package/dist/core/atlasPlan.js.map +0 -1
  522. package/dist/core/eases.js.map +0 -1
  523. package/dist/core/gameObject.js.map +0 -1
  524. package/dist/core/pixiPatches.js.map +0 -1
  525. package/dist/core/timelinePlace.js.map +0 -1
  526. package/dist/core/tweenProps.js.map +0 -1
  527. package/dist/debug/DebugOverlay.js.map +0 -1
  528. package/dist/debug/DrawCallCounter.js.map +0 -1
  529. package/dist/debug/ErrorLog.js.map +0 -1
  530. package/dist/debug/FrameStats.js.map +0 -1
  531. package/dist/debug/inspectWorld.js.map +0 -1
  532. package/dist/diagnostics/ErrorDeduper.js.map +0 -1
  533. package/dist/diagnostics/FrameSampler.js.map +0 -1
  534. package/dist/diagnostics/bootWatch.js.map +0 -1
  535. package/dist/diagnostics/captureErrors.js.map +0 -1
  536. package/dist/diagnostics/contextLoss.js.map +0 -1
  537. package/dist/diagnostics/frameStats.js.map +0 -1
  538. package/dist/diagnostics/gpuInfo.js.map +0 -1
  539. package/dist/diagnostics/renderGuard.js.map +0 -1
  540. package/dist/ecs/SparseSet.js.map +0 -1
  541. package/dist/ecs/World.js.map +0 -1
  542. package/dist/ecs/fields.js.map +0 -1
  543. package/dist/engine/Engine.js.map +0 -1
  544. package/dist/engine/FixedLoop.js.map +0 -1
  545. package/dist/engine/Scene.js.map +0 -1
  546. package/dist/engine/SceneManager.js.map +0 -1
  547. package/dist/events/EventBus.js.map +0 -1
  548. package/dist/fx/FloatingText.js.map +0 -1
  549. package/dist/fx/hitStop.js.map +0 -1
  550. package/dist/fx/viewCull.js.map +0 -1
  551. package/dist/i18n/Translator.js.map +0 -1
  552. package/dist/i18n/extractStrings.js.map +0 -1
  553. package/dist/i18n/formatMessage.js.map +0 -1
  554. package/dist/input/Actions.js.map +0 -1
  555. package/dist/input/AimGesture.js.map +0 -1
  556. package/dist/input/Keyboard.js.map +0 -1
  557. package/dist/input/Keymap.js.map +0 -1
  558. package/dist/input/TargetLock.js.map +0 -1
  559. package/dist/input/aimAxis.js.map +0 -1
  560. package/dist/input/keyChord.js.map +0 -1
  561. package/dist/input/longPress.js.map +0 -1
  562. package/dist/input/stickAxis.js.map +0 -1
  563. package/dist/input/touchDetect.js.map +0 -1
  564. package/dist/light/LightLayer.js.map +0 -1
  565. package/dist/nav/MinHeap.js.map +0 -1
  566. package/dist/nav/findPath.js.map +0 -1
  567. package/dist/nav/navGrid.js.map +0 -1
  568. package/dist/nav/nearestOpen.js.map +0 -1
  569. package/dist/nav/straightenPath.js.map +0 -1
  570. package/dist/net/Connection.js.map +0 -1
  571. package/dist/net/InterpolationBuffer.js.map +0 -1
  572. package/dist/net/PredictedMover.js.map +0 -1
  573. package/dist/net/Room.js.map +0 -1
  574. package/dist/net/RoomHost.js.map +0 -1
  575. package/dist/net/cadence.js.map +0 -1
  576. package/dist/net/constants.js.map +0 -1
  577. package/dist/net/lobby/LobbyClient.js.map +0 -1
  578. package/dist/net/lobby/LobbyHost.js.map +0 -1
  579. package/dist/net/lobby/LobbyRooms.js.map +0 -1
  580. package/dist/net/lobby/Matchmaker.js.map +0 -1
  581. package/dist/net/movement.js.map +0 -1
  582. package/dist/net/protocol.js.map +0 -1
  583. package/dist/net/serverClock.js.map +0 -1
  584. package/dist/net/snapshotCodec.js.map +0 -1
  585. package/dist/parallax/Parallax.js.map +0 -1
  586. package/dist/parallax/ParallaxLayer.js.map +0 -1
  587. package/dist/parallax/parallaxMath.js.map +0 -1
  588. package/dist/particles/Emitter.js.map +0 -1
  589. package/dist/particles/ParticleBudget.js.map +0 -1
  590. package/dist/particles/ParticleSim.js.map +0 -1
  591. package/dist/particles/particleCurves.js.map +0 -1
  592. package/dist/particles/particleSampling.js.map +0 -1
  593. package/dist/physics/Body.js.map +0 -1
  594. package/dist/physics/BroadPhase.js.map +0 -1
  595. package/dist/physics/Contact.js.map +0 -1
  596. package/dist/physics/ContactSolver.js.map +0 -1
  597. package/dist/physics/PhysicsWorld.js.map +0 -1
  598. package/dist/physics/SpatialHash.js.map +0 -1
  599. package/dist/physics/collide.js.map +0 -1
  600. package/dist/physics/collidePolygon.js.map +0 -1
  601. package/dist/physics/contactReport.js.map +0 -1
  602. package/dist/physics/drawColliders.js.map +0 -1
  603. package/dist/physics/physicsSystem.js.map +0 -1
  604. package/dist/physics/polygon.js.map +0 -1
  605. package/dist/physics/raycast.js.map +0 -1
  606. package/dist/physics/syncToBody.js.map +0 -1
  607. package/dist/physics/tileBodies.js.map +0 -1
  608. package/dist/physics/types.js.map +0 -1
  609. package/dist/pixel/Darkness.js.map +0 -1
  610. package/dist/pixel/DitherVignette.js.map +0 -1
  611. package/dist/pixel/Evaporation.js.map +0 -1
  612. package/dist/pixel/NightShade.js.map +0 -1
  613. package/dist/pixel/PixelLayer.js.map +0 -1
  614. package/dist/pixel/SwayProp.js.map +0 -1
  615. package/dist/pixel/lights.js.map +0 -1
  616. package/dist/pixel/nineSlice.js.map +0 -1
  617. package/dist/pixel/paintLayer.js.map +0 -1
  618. package/dist/pixel/palette.js.map +0 -1
  619. package/dist/pixel/pixel.js.map +0 -1
  620. package/dist/pixel/pixelPaint.js.map +0 -1
  621. package/dist/pixel/pixelTone.js.map +0 -1
  622. package/dist/pixel/propKit.js.map +0 -1
  623. package/dist/pixel/ramp.js.map +0 -1
  624. package/dist/pixel/selout.js.map +0 -1
  625. package/dist/pixel/spritePixels.js.map +0 -1
  626. package/dist/pixel/swayRig.js.map +0 -1
  627. package/dist/pixel/vignetteMask.js.map +0 -1
  628. package/dist/pixel/weatherArt.js.map +0 -1
  629. package/dist/pixel.js.map +0 -1
  630. package/dist/platform/touch.js.map +0 -1
  631. package/dist/postfx/BloomFilter.js.map +0 -1
  632. package/dist/postfx/ChromaticAberrationFilter.js.map +0 -1
  633. package/dist/postfx/ColorAdjustFilter.js.map +0 -1
  634. package/dist/postfx/CrtFilter.js.map +0 -1
  635. package/dist/postfx/PixelateFilter.js.map +0 -1
  636. package/dist/postfx/PostFx.js.map +0 -1
  637. package/dist/postfx/ShockwaveFilter.js.map +0 -1
  638. package/dist/postfx/VignetteFilter.js.map +0 -1
  639. package/dist/postfx/fxAnimator.js.map +0 -1
  640. package/dist/postfx/fxShader.js.map +0 -1
  641. package/dist/postfx.js.map +0 -1
  642. package/dist/react/ChoiceDialog.js.map +0 -1
  643. package/dist/react/ConfirmDialog.js.map +0 -1
  644. package/dist/react/Glyph.js.map +0 -1
  645. package/dist/react/Modal.js.map +0 -1
  646. package/dist/react/Panel.js.map +0 -1
  647. package/dist/react/PixelImage.js.map +0 -1
  648. package/dist/react/Select.js.map +0 -1
  649. package/dist/react/Slider.js.map +0 -1
  650. package/dist/react/SplitPane.js.map +0 -1
  651. package/dist/react/TabStrip.js.map +0 -1
  652. package/dist/react/TitleTips.js.map +0 -1
  653. package/dist/react/Tooltip.js.map +0 -1
  654. package/dist/react/UiProvider.js.map +0 -1
  655. package/dist/react/baseCss.js.map +0 -1
  656. package/dist/react/choiceStore.js.map +0 -1
  657. package/dist/react/classNames.js.map +0 -1
  658. package/dist/react/confirmStore.js.map +0 -1
  659. package/dist/react/labels.js.map +0 -1
  660. package/dist/react/placement.js.map +0 -1
  661. package/dist/react/useFillSheet.js.map +0 -1
  662. package/dist/react/useFitToViewport.js.map +0 -1
  663. package/dist/react/usePicked.js.map +0 -1
  664. package/dist/react/useStore.js.map +0 -1
  665. package/dist/react/useViewport.js.map +0 -1
  666. package/dist/react.js.map +0 -1
  667. package/dist/rig/PosePlayer.js.map +0 -1
  668. package/dist/rig/RigView.js.map +0 -1
  669. package/dist/rig/Skeleton.js.map +0 -1
  670. package/dist/rig/humanoid.js.map +0 -1
  671. package/dist/rig/humanoid.json.js.map +0 -1
  672. package/dist/rig/humanoidPoses.js.map +0 -1
  673. package/dist/rig/poseMath.js.map +0 -1
  674. package/dist/rig/poses.json.js.map +0 -1
  675. package/dist/save/Autosaver.js.map +0 -1
  676. package/dist/save/MemoryBackend.js.map +0 -1
  677. package/dist/save/SaveStore.js.map +0 -1
  678. package/dist/save/WebStorageBackend.js.map +0 -1
  679. package/dist/save/checksum.js.map +0 -1
  680. package/dist/save/migrateSave.js.map +0 -1
  681. package/dist/settings.js.map +0 -1
  682. package/dist/tilemap/TilemapView.js.map +0 -1
  683. package/dist/tilemap/collision.js.map +0 -1
  684. package/dist/tilemap/tiled.js.map +0 -1
  685. package/dist/transitions/CrossfadeTransition.js.map +0 -1
  686. package/dist/transitions/DitherWipeTransition.js.map +0 -1
  687. package/dist/transitions/FadeTransition.js.map +0 -1
  688. package/dist/transitions/Letterbox.js.map +0 -1
  689. package/dist/transitions/ditherMask.js.map +0 -1
  690. package/dist/transitions/transitionTo.js.map +0 -1
  691. package/dist/transitions/tweenDone.js.map +0 -1
  692. package/dist/ui/Bar.js.map +0 -1
  693. package/dist/ui/Button.js.map +0 -1
  694. package/dist/ui/Dialog.js.map +0 -1
  695. package/dist/ui/FocusManager.js.map +0 -1
  696. package/dist/ui/Label.js.map +0 -1
  697. package/dist/ui/List.js.map +0 -1
  698. package/dist/ui/Panel.js.map +0 -1
  699. package/dist/ui/ScrollView.js.map +0 -1
  700. package/dist/ui/Slider.js.map +0 -1
  701. package/dist/ui/Stack.js.map +0 -1
  702. package/dist/ui/Toggle.js.map +0 -1
  703. package/dist/ui/UiHost.js.map +0 -1
  704. package/dist/ui/UiPointerGate.js.map +0 -1
  705. package/dist/ui/UiRoot.js.map +0 -1
  706. package/dist/ui/Widget.js.map +0 -1
  707. package/dist/ui/dom/DomOverlay.js.map +0 -1
  708. package/dist/ui/dom/DomWorldAnchors.js.map +0 -1
  709. package/dist/ui/dom/InputShield.js.map +0 -1
  710. package/dist/ui/dom/createStore.js.map +0 -1
  711. package/dist/ui/dom/shieldDecision.js.map +0 -1
  712. package/dist/ui/dom/worldToScreen.js.map +0 -1
  713. package/dist/ui/layout/anchor.js.map +0 -1
  714. package/dist/ui/layout/barDrain.js.map +0 -1
  715. package/dist/ui/layout/fitToViewport.js.map +0 -1
  716. package/dist/ui/layout/focusNav.js.map +0 -1
  717. package/dist/ui/layout/geometry.js.map +0 -1
  718. package/dist/ui/layout/listWidth.js.map +0 -1
  719. package/dist/ui/layout/placeTooltip.js.map +0 -1
  720. package/dist/ui/layout/scroll.js.map +0 -1
  721. package/dist/ui/layout/slider.js.map +0 -1
  722. package/dist/ui/layout/stack.js.map +0 -1
  723. package/dist/ui/layout/wrapRow.js.map +0 -1
  724. package/dist/ui.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"Engine.js","sources":["../../src/engine/Engine.ts"],"sourcesContent":["import { Application, TextureSource } from 'pixi.js';\nimport { Stage } from '../core/Stage';\nimport { TextureRegistry } from '../core/TextureRegistry';\nimport { patchPixi } from '../core/pixiPatches';\nimport { EventBus } from '../events/EventBus';\nimport { FixedLoop } from './FixedLoop';\nimport { SceneManager } from './SceneManager';\nimport type { Scene } from './Scene';\n\nexport interface EngineOptions {\n /** Where the canvas goes. */\n parent: HTMLElement;\n /** A fixed canvas size in CSS pixels. Leave both out to fill `parent` and follow its size. */\n width?: number;\n height?: number;\n /** CSS colour behind the world (default black). */\n background?: string;\n /** World pixels are drawn this many screen pixels wide (default 1). Whole numbers keep pixel art crisp. */\n zoom?: number;\n /** Simulation steps per second (default 60). */\n hz?: number;\n /** Textures shared by the stage; a fresh registry by default. */\n textures?: TextureRegistry;\n /**\n * Pixel art: nearest-neighbour textures, no antialiasing, one backing pixel per CSS pixel, and a camera that lands\n * on whole pixels (default false: smooth textures drawn at the screen's own density, for any other art style).\n */\n pixelPerfect?: boolean;\n /** Backing pixels per CSS pixel when not `pixelPerfect` (default `devicePixelRatio`, at most 2). */\n resolution?: number;\n /**\n * Renderer preference (default 'webgl'). WebGL 2 runs everywhere the engine is tested. 'webgpu' is experimental:\n * Pixi falls back to WebGL where WebGPU is missing, but the engine's own paths (pooled `PixelLayer` buffers, the\n * `erase` blend `Darkness` cuts lights with, the fill filters) have only been profiled on WebGL.\n */\n preference?: 'webgl' | 'webgpu';\n}\n\nexport interface EngineEvents {\n /** Something a guarded step or a system threw; the frame carried on. */\n error: { where: string; error: unknown };\n /** After every rendered frame's update, before Pixi renders. */\n frame: { now: number; dt: number };\n}\n\n/** The stage the engine drives: its per-frame update is the engine's tick. */\nclass EngineStage extends Stage {\n constructor(\n app: Application,\n textures: TextureRegistry,\n private readonly tick: (now: number, delta: number) => void,\n ) {\n super(app, textures);\n }\n\n override update(now: number, delta: number): void {\n this.tick(now, delta);\n }\n}\n\n/**\n * The top of the engine: a Pixi application, the `Stage` (world and screen layers, camera, tweens, timers,\n * pointer), a fixed-step simulation loop and a stack of scenes.\n *\n * ```ts\n * const engine = await Engine.create({ parent: document.body, zoom: 3 });\n * await engine.scenes.replace(new MyLevel());\n * ```\n */\nexport class Engine {\n readonly stage: Stage;\n readonly loop: FixedLoop;\n readonly scenes: SceneManager;\n readonly events = new EventBus<EngineEvents>();\n private running = true;\n private destroyed = false;\n private readonly onTick = (t: { deltaMS: number }) => this.stage.frame(t.deltaMS);\n\n private constructor(\n readonly app: Application,\n textures: TextureRegistry,\n options: EngineOptions,\n ) {\n this.loop = new FixedLoop(options.hz === undefined ? {} : { hz: options.hz });\n this.stage = new EngineStage(app, textures, (now, delta) => this.tick(now, delta));\n const cam = this.stage.cameras.main;\n cam.setZoom(options.zoom ?? 1);\n cam.defaultZoom = cam.zoom;\n cam.roundPixels = options.pixelPerfect === true;\n cam.resolution = app.renderer.resolution;\n this.scenes = new SceneManager(this);\n Stage.onCaught = (where, error) => this.events.emit('error', { where, error });\n app.ticker.add(this.onTick);\n }\n\n static async create(options: EngineOptions): Promise<Engine> {\n const pixel = options.pixelPerfect === true;\n TextureSource.defaultOptions.scaleMode = pixel ? 'nearest' : 'linear';\n const resolution = pixel ? 1 : (options.resolution ?? Math.min(2, typeof devicePixelRatio === 'number' ? devicePixelRatio : 1));\n patchPixi();\n const app = new Application();\n const fill = options.width === undefined && options.height === undefined;\n await app.init({\n ...(fill ? { resizeTo: options.parent } : { width: options.width ?? 800, height: options.height ?? 600 }),\n background: options.background ?? '#000000',\n antialias: !pixel,\n resolution,\n autoDensity: true,\n preference: options.preference ?? 'webgl',\n });\n options.parent.appendChild(app.canvas);\n return new Engine(app, options.textures ?? new TextureRegistry(), options);\n }\n\n /** The scene that updates now, if any. */\n get scene(): Scene | undefined {\n return this.scenes.current;\n }\n\n /** Stops simulating and drawing; the canvas keeps its last frame. */\n pause(): void {\n if (!this.running) return;\n this.running = false;\n this.app.ticker.stop();\n }\n\n resume(): void {\n if (this.running || this.destroyed) return;\n this.running = true;\n this.loop.reset();\n this.app.ticker.start();\n }\n\n get paused(): boolean {\n return !this.running;\n }\n\n /** Reports a fault to `events.error` (and the console when nobody listens). */\n reportError(where: string, error: unknown): void {\n if (this.events.listenerCount('error') === 0) console.error(`[engine] ${where}`, error);\n this.events.emit('error', { where, error });\n }\n\n /** Leaves every scene, removes the canvas and frees the renderer. */\n async destroy(): Promise<void> {\n if (this.destroyed) return;\n this.destroyed = true;\n await this.scenes.clear();\n this.app.ticker.remove(this.onTick);\n if (Stage.onCaught) Stage.onCaught = null;\n this.stage.destroy();\n this.events.clear();\n this.app.destroy({ removeView: true }, { children: true });\n }\n\n private tick(now: number, delta: number): void {\n const scene = this.scenes.current;\n if (scene) {\n const report = (s: { name: string }, err: unknown) => this.reportError(`system ${s.name}`, err);\n const step = this.loop.stepSeconds;\n const alpha = this.loop.advance(delta, () => {\n scene.world.run('fixed', { dt: step, now, alpha: 0 }, report);\n scene.fixedUpdate(step);\n });\n const dt = delta / 1000;\n scene.world.run('frame', { dt, now, alpha }, report);\n scene.update(dt, alpha);\n }\n this.events.emit('frame', { now, dt: delta / 1000 });\n }\n}\n"],"names":["EngineStage","Stage","app","textures","tick","now","delta","Engine","options","FixedLoop","cam","SceneManager","where","error","EventBus","t","pixel","TextureSource","resolution","patchPixi","Application","fill","TextureRegistry","scene","report","s","err","step","alpha","dt"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"FixedLoop.js","sources":["../../src/engine/FixedLoop.ts"],"sourcesContent":["/**\n * A fixed-timestep accumulator: frames arrive whenever the display wants them, the simulation advances in equal\n * steps. Movement and anything a server replays must run here, so every machine takes the same steps whatever its\n * frame rate.\n *\n * A frame that comes very late (a tab in the background, a debugger) would owe hundreds of steps; at most\n * `maxStepsPerFrame` are run and the rest of the debt is dropped, so the game slows down instead of freezing while it\n * catches up (the \"spiral of death\").\n */\nexport interface FixedLoopOptions {\n /** Steps per second (default 60). */\n hz?: number;\n /** The most steps one frame may run before the debt is dropped (default 8). */\n maxStepsPerFrame?: number;\n}\n\nexport class FixedLoop {\n readonly stepMs: number;\n readonly stepSeconds: number;\n private readonly maxSteps: number;\n private carry = 0;\n /** Steps taken since the loop started. */\n tick = 0;\n\n constructor(options: FixedLoopOptions = {}) {\n const hz = options.hz ?? 60;\n if (!(hz > 0)) throw new Error(`FixedLoop: hz must be positive, got ${hz}`);\n this.stepMs = 1000 / hz;\n this.stepSeconds = 1 / hz;\n this.maxSteps = Math.max(1, options.maxStepsPerFrame ?? 8);\n }\n\n /**\n * Feeds one frame of `deltaMs` and runs `step` as many times as it owes. Returns the share of a step left over\n * (0–1), for interpolating what is drawn between the last two steps.\n */\n advance(deltaMs: number, step: (tick: number) => void): number {\n if (deltaMs > 0 && Number.isFinite(deltaMs)) this.carry += deltaMs;\n let ran = 0;\n while (this.carry >= this.stepMs) {\n if (ran === this.maxSteps) {\n this.carry %= this.stepMs;\n break;\n }\n this.carry -= this.stepMs;\n ran++;\n step(this.tick++);\n }\n return this.carry / this.stepMs;\n }\n\n /** Forgets any debt, e.g. after a scene change or a long load. */\n reset(): void {\n this.carry = 0;\n }\n}\n"],"names":["FixedLoop","options","hz","deltaMs","step","ran"],"mappings":"AAgBO,MAAMA,EAAU;AAAA,EACZ;AAAA,EACA;AAAA,EACQ;AAAA,EACT,QAAQ;AAAA;AAAA,EAEhB,OAAO;AAAA,EAEP,YAAYC,IAA4B,IAAI;AAC1C,UAAMC,IAAKD,EAAQ,MAAM;AACzB,QAAI,EAAEC,IAAK,GAAI,OAAM,IAAI,MAAM,uCAAuCA,CAAE,EAAE;AAC1E,SAAK,SAAS,MAAOA,GACrB,KAAK,cAAc,IAAIA,GACvB,KAAK,WAAW,KAAK,IAAI,GAAGD,EAAQ,oBAAoB,CAAC;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAQE,GAAiBC,GAAsC;AAC7D,IAAID,IAAU,KAAK,OAAO,SAASA,CAAO,WAAQ,SAASA;AAC3D,QAAIE,IAAM;AACV,WAAO,KAAK,SAAS,KAAK,UAAQ;AAChC,UAAIA,MAAQ,KAAK,UAAU;AACzB,aAAK,SAAS,KAAK;AACnB;AAAA,MACF;AACA,WAAK,SAAS,KAAK,QACnBA,KACAD,EAAK,KAAK,MAAM;AAAA,IAClB;AACA,WAAO,KAAK,QAAQ,KAAK;AAAA,EAC3B;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,QAAQ;AAAA,EACf;AACF;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"Scene.js","sources":["../../src/engine/Scene.ts"],"sourcesContent":["import { Container } from 'pixi.js';\nimport { World } from '../ecs/World';\nimport type { Engine } from './Engine';\n\n/** Input a scene can be told to forget its pending presses on (an `Actions`). */\nexport interface FlushableInput {\n flush(): void;\n}\n\n/** A timer `Scene.delay` started; `remove` cancels it. */\nexport interface SceneTimer {\n remove(): void;\n}\n\n/**\n * One screen of the game: a title, a level, a pause menu. A scene owns a `World` of entities and systems and a\n * `root` container in the engine's world layer; both are cleared when it leaves, so nothing it made outlives it.\n *\n * Override the hooks you need. `fixedUpdate` runs once per simulation step, `update` once per rendered frame, both\n * after the world's own systems of that phase.\n *\n * Two things a scene should hand the engine so they end with it: timers (`delay`, not `stage.time.delayedCall`, whose\n * timers belong to the stage and fire after the scene has gone) and its input (`useInput`, flushed when a scene pushed\n * over this one pops, so presses made in a pause menu are not read again by the level under it).\n *\n * Names taken: `world`, `root` and `hud` are the scene's own fields, so a subclass cannot declare a field of those\n * names for something else (a HUD class of the game's is best kept as `display`, `overlay`, …).\n */\nexport abstract class Scene {\n readonly world = new World();\n /** Everything the scene draws in the world goes under this (the camera moves it). */\n readonly root = new Container({ sortableChildren: true });\n /** Everything the scene draws on the screen (HUD) goes under this. */\n readonly hud = new Container();\n private bound: Engine | null = null;\n private readonly timers = new Set<SceneTimer>();\n private readonly inputs: FlushableInput[] = [];\n\n /** The engine running this scene; only valid between `enter` and `exit`. */\n get engine(): Engine {\n if (!this.bound) throw new Error(`${this.constructor.name}: not running in an engine`);\n return this.bound;\n }\n\n /** Called once when the scene becomes active. May be async: the scene starts updating when it resolves. */\n onEnter(): void | Promise<void> {}\n /** Called when the scene is removed. The world and containers are cleared after it returns. */\n onExit(): void {}\n /** Called when another scene is pushed over this one. */\n onPause(): void {}\n /** Called when the scene over this one is popped. */\n onResume(): void {}\n /** Once per simulation step, `dt` in seconds. */\n fixedUpdate(_dt: number): void {}\n /** Once per rendered frame, `dt` in seconds; `alpha` is the share of a step since the last one (0–1). */\n update(_dt: number, _alpha: number): void {}\n\n /**\n * Calls `fn` after `ms` on the stage's clock, unless the scene has left by then: its timers are cancelled when it\n * exits. Only valid while the scene is running (between `enter` and `exit`).\n */\n delay(ms: number, fn: () => void): SceneTimer {\n const handle = this.engine.stage.time.delayedCall(ms, () => {\n this.timers.delete(timer);\n fn();\n });\n const timer: SceneTimer = {\n remove: () => {\n this.timers.delete(timer);\n handle.remove();\n },\n };\n this.timers.add(timer);\n return timer;\n }\n\n /**\n * Registers input the scene reads (an `Actions`) and returns it, so a field can be written in one line:\n * `private readonly input = this.useInput(new Actions(keyboard, bindings));`. When a scene pushed over this one\n * pops, every registered input is flushed before `onResume`, so a key pressed in the menu is not a press here.\n */\n useInput<T extends FlushableInput>(input: T): T {\n this.inputs.push(input);\n return input;\n }\n\n /** @internal Called by the scene manager before `onResume`. */\n flushInput(): void {\n for (const i of this.inputs) i.flush();\n }\n\n /** @internal Engine-side wiring; games call `engine.scenes`. */\n attach(engine: Engine): void {\n this.bound = engine;\n engine.stage.world.addChild(this.root);\n engine.stage.screen.addChild(this.hud);\n }\n\n /** @internal Clears what the scene made but keeps its containers, so the same scene may be entered again. */\n detach(): void {\n for (const t of [...this.timers]) t.remove();\n this.world.clear();\n for (const c of [this.root, this.hud]) {\n for (const child of c.removeChildren()) child.destroy({ children: true });\n c.removeFromParent();\n }\n this.bound = null;\n }\n}\n"],"names":["Scene","World","Container","_dt","_alpha","ms","fn","handle","timer","input","i","engine","c","child"],"mappings":";;AA4BO,MAAeA,EAAM;AAAA,EACjB,QAAQ,IAAIC,EAAA;AAAA;AAAA,EAEZ,OAAO,IAAIC,EAAU,EAAE,kBAAkB,IAAM;AAAA;AAAA,EAE/C,MAAM,IAAIA,EAAA;AAAA,EACX,QAAuB;AAAA,EACd,6BAAa,IAAA;AAAA,EACb,SAA2B,CAAA;AAAA;AAAA,EAG5C,IAAI,SAAiB;AACnB,QAAI,CAAC,KAAK,MAAO,OAAM,IAAI,MAAM,GAAG,KAAK,YAAY,IAAI,4BAA4B;AACrF,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,UAAgC;AAAA,EAAC;AAAA;AAAA,EAEjC,SAAe;AAAA,EAAC;AAAA;AAAA,EAEhB,UAAgB;AAAA,EAAC;AAAA;AAAA,EAEjB,WAAiB;AAAA,EAAC;AAAA;AAAA,EAElB,YAAYC,GAAmB;AAAA,EAAC;AAAA;AAAA,EAEhC,OAAOA,GAAaC,GAAsB;AAAA,EAAC;AAAA;AAAA;AAAA;AAAA;AAAA,EAM3C,MAAMC,GAAYC,GAA4B;AAC5C,UAAMC,IAAS,KAAK,OAAO,MAAM,KAAK,YAAYF,GAAI,MAAM;AAC1D,WAAK,OAAO,OAAOG,CAAK,GACxBF,EAAA;AAAA,IACF,CAAC,GACKE,IAAoB;AAAA,MACxB,QAAQ,MAAM;AACZ,aAAK,OAAO,OAAOA,CAAK,GACxBD,EAAO,OAAA;AAAA,MACT;AAAA,IAAA;AAEF,gBAAK,OAAO,IAAIC,CAAK,GACdA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,SAAmCC,GAAa;AAC9C,gBAAK,OAAO,KAAKA,CAAK,GACfA;AAAA,EACT;AAAA;AAAA,EAGA,aAAmB;AACjB,eAAWC,KAAK,KAAK,OAAQ,CAAAA,EAAE,MAAA;AAAA,EACjC;AAAA;AAAA,EAGA,OAAOC,GAAsB;AAC3B,SAAK,QAAQA,GACbA,EAAO,MAAM,MAAM,SAAS,KAAK,IAAI,GACrCA,EAAO,MAAM,OAAO,SAAS,KAAK,GAAG;AAAA,EACvC;AAAA;AAAA,EAGA,SAAe;AACb,eAAW,KAAK,CAAC,GAAG,KAAK,MAAM,KAAK,OAAA;AACpC,SAAK,MAAM,MAAA;AACX,eAAWC,KAAK,CAAC,KAAK,MAAM,KAAK,GAAG,GAAG;AACrC,iBAAWC,KAASD,EAAE,eAAA,KAAwB,QAAQ,EAAE,UAAU,IAAM;AACxE,MAAAA,EAAE,iBAAA;AAAA,IACJ;AACA,SAAK,QAAQ;AAAA,EACf;AACF;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"SceneManager.js","sources":["../../src/engine/SceneManager.ts"],"sourcesContent":["import type { Engine } from './Engine';\nimport type { Scene } from './Scene';\n\n/**\n * A stack of scenes. Only the top one updates; the ones under it are paused but still drawn (a pause menu over the\n * level). Changes are queued and applied one at a time, so a scene that replaces itself from inside its own update,\n * or an async `onEnter` still loading, never leaves two scenes half entered.\n *\n * The camera is the engine's, not a scene's: `replace` resets it (`Camera.reset`: no target, no bounds, the default\n * zoom) after the old scenes leave and before the new one enters, so a level's follow and bounds never hold over a\n * title screen. `push` and `pop` leave it as it is: the scene under a pause menu keeps its view. `pop` flushes the\n * resumed scene's input (`Scene.useInput`) before its `onResume`.\n */\nexport class SceneManager {\n private readonly stack: Scene[] = [];\n private queue: Promise<void> = Promise.resolve();\n /** True while an async `onEnter` is running: nothing updates until it is done. */\n private entering = false;\n\n constructor(private readonly engine: Engine) {}\n\n /** The scene that updates, if any. */\n get current(): Scene | undefined {\n return this.entering ? undefined : this.stack[this.stack.length - 1];\n }\n\n /** Every scene on the stack, bottom first. */\n get all(): readonly Scene[] {\n return this.stack;\n }\n\n /** Removes every scene and enters `scene`. */\n replace(scene: Scene): Promise<void> {\n return this.enqueue(async () => {\n while (this.stack.length) this.exitTop();\n this.engine.stage.cameras.main.reset();\n await this.enter(scene);\n });\n }\n\n /** Pauses the current scene and enters `scene` over it. */\n push(scene: Scene): Promise<void> {\n return this.enqueue(async () => {\n this.stack[this.stack.length - 1]?.onPause();\n await this.enter(scene);\n });\n }\n\n /** Leaves the current scene and resumes the one under it. */\n pop(): Promise<void> {\n return this.enqueue(async () => {\n if (!this.stack.length) return;\n this.exitTop();\n const under = this.stack[this.stack.length - 1];\n if (!under) return;\n under.flushInput();\n under.onResume();\n });\n }\n\n /** Leaves every scene (the engine is shutting down). */\n clear(): Promise<void> {\n return this.enqueue(async () => {\n while (this.stack.length) this.exitTop();\n });\n }\n\n private async enter(scene: Scene): Promise<void> {\n if (this.stack.includes(scene)) throw new Error(`${scene.constructor.name} is already on the scene stack`);\n scene.attach(this.engine);\n this.stack.push(scene);\n this.entering = true;\n try {\n await scene.onEnter();\n } finally {\n this.entering = false;\n }\n this.engine.loop.reset();\n }\n\n private exitTop(): void {\n const scene = this.stack.pop();\n if (!scene) return;\n try {\n scene.onExit();\n } finally {\n scene.detach();\n }\n }\n\n private enqueue(job: () => Promise<void>): Promise<void> {\n const run = this.queue.then(job);\n // a failed change must not wedge every change after it\n this.queue = run.catch((err) => this.engine.reportError('scene', err));\n return run;\n }\n}\n"],"names":["SceneManager","engine","scene","under","job","run","err"],"mappings":"AAaO,MAAMA,EAAa;AAAA,EAMxB,YAA6BC,GAAgB;AAAhB,SAAA,SAAAA;AAAA,EAAiB;AAAA,EAAjB;AAAA,EALZ,QAAiB,CAAA;AAAA,EAC1B,QAAuB,QAAQ,QAAA;AAAA;AAAA,EAE/B,WAAW;AAAA;AAAA,EAKnB,IAAI,UAA6B;AAC/B,WAAO,KAAK,WAAW,SAAY,KAAK,MAAM,KAAK,MAAM,SAAS,CAAC;AAAA,EACrE;AAAA;AAAA,EAGA,IAAI,MAAwB;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,QAAQC,GAA6B;AACnC,WAAO,KAAK,QAAQ,YAAY;AAC9B,aAAO,KAAK,MAAM,SAAQ,MAAK,QAAA;AAC/B,WAAK,OAAO,MAAM,QAAQ,KAAK,MAAA,GAC/B,MAAM,KAAK,MAAMA,CAAK;AAAA,IACxB,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,KAAKA,GAA6B;AAChC,WAAO,KAAK,QAAQ,YAAY;AAC9B,WAAK,MAAM,KAAK,MAAM,SAAS,CAAC,GAAG,QAAA,GACnC,MAAM,KAAK,MAAMA,CAAK;AAAA,IACxB,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAqB;AACnB,WAAO,KAAK,QAAQ,YAAY;AAC9B,UAAI,CAAC,KAAK,MAAM,OAAQ;AACxB,WAAK,QAAA;AACL,YAAMC,IAAQ,KAAK,MAAM,KAAK,MAAM,SAAS,CAAC;AAC9C,MAAKA,MACLA,EAAM,WAAA,GACNA,EAAM,SAAA;AAAA,IACR,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,QAAuB;AACrB,WAAO,KAAK,QAAQ,YAAY;AAC9B,aAAO,KAAK,MAAM,SAAQ,MAAK,QAAA;AAAA,IACjC,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,MAAMD,GAA6B;AAC/C,QAAI,KAAK,MAAM,SAASA,CAAK,EAAG,OAAM,IAAI,MAAM,GAAGA,EAAM,YAAY,IAAI,gCAAgC;AACzG,IAAAA,EAAM,OAAO,KAAK,MAAM,GACxB,KAAK,MAAM,KAAKA,CAAK,GACrB,KAAK,WAAW;AAChB,QAAI;AACF,YAAMA,EAAM,QAAA;AAAA,IACd,UAAA;AACE,WAAK,WAAW;AAAA,IAClB;AACA,SAAK,OAAO,KAAK,MAAA;AAAA,EACnB;AAAA,EAEQ,UAAgB;AACtB,UAAMA,IAAQ,KAAK,MAAM,IAAA;AACzB,QAAKA;AACL,UAAI;AACF,QAAAA,EAAM,OAAA;AAAA,MACR,UAAA;AACE,QAAAA,EAAM,OAAA;AAAA,MACR;AAAA,EACF;AAAA,EAEQ,QAAQE,GAAyC;AACvD,UAAMC,IAAM,KAAK,MAAM,KAAKD,CAAG;AAE/B,gBAAK,QAAQC,EAAI,MAAM,CAACC,MAAQ,KAAK,OAAO,YAAY,SAASA,CAAG,CAAC,GAC9DD;AAAA,EACT;AACF;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"EventBus.js","sources":["../../src/events/EventBus.ts"],"sourcesContent":["/**\n * Typed observer: listeners subscribe per event name and run in subscription order.\n * `Events` maps each event name to its payload, e.g. `EventBus<{ enemyKilled: { id: number } }>`.\n *\n * A listener that throws does not stop the ones after it; the error is handed to `onError` (the console by default).\n */\nexport class EventBus<Events extends object> {\n private readonly listeners = new Map<keyof Events, ((payload: never) => void)[]>();\n\n constructor(private readonly onError: (type: keyof Events, err: unknown) => void = (type, err) => console.error(`[events] ${String(type)} listener failed`, err)) {}\n\n /** Subscribes to `type`; returns the unsubscribe function. */\n on<K extends keyof Events>(type: K, fn: (payload: Events[K]) => void): () => void {\n this.listeners.set(type, [...(this.listeners.get(type) ?? []), fn]);\n return () => this.off(type, fn);\n }\n\n /** Subscribes for the next `type` only. */\n once<K extends keyof Events>(type: K, fn: (payload: Events[K]) => void): () => void {\n const off = this.on(type, (payload) => {\n off();\n fn(payload);\n });\n return off;\n }\n\n off<K extends keyof Events>(type: K, fn: (payload: Events[K]) => void): void {\n const list = this.listeners.get(type);\n if (!list) return;\n const left = list.filter((f) => f !== fn);\n if (left.length) this.listeners.set(type, left);\n else this.listeners.delete(type);\n }\n\n emit<K extends keyof Events>(type: K, payload: Events[K]): void {\n // the list is replaced, never mutated, on every (un)subscribe: walking this one is safe while listeners change it\n for (const fn of this.listeners.get(type) ?? []) {\n try {\n (fn as (payload: Events[K]) => void)(payload);\n } catch (err) {\n this.onError(type, err);\n }\n }\n }\n\n listenerCount(type: keyof Events): number {\n return this.listeners.get(type)?.length ?? 0;\n }\n\n clear(): void {\n this.listeners.clear();\n }\n}\n"],"names":["EventBus","onError","type","err","fn","off","payload","list","left","f"],"mappings":"AAMO,MAAMA,EAAgC;AAAA,EAG3C,YAA6BC,IAAsD,CAACC,GAAMC,MAAQ,QAAQ,MAAM,YAAY,OAAOD,CAAI,CAAC,oBAAoBC,CAAG,GAAG;AAArI,SAAA,UAAAF;AAAA,EAAsI;AAAA,EAAtI;AAAA,EAFZ,gCAAgB,IAAA;AAAA;AAAA,EAKjC,GAA2BC,GAASE,GAA8C;AAChF,gBAAK,UAAU,IAAIF,GAAM,CAAC,GAAI,KAAK,UAAU,IAAIA,CAAI,KAAK,CAAA,GAAKE,CAAE,CAAC,GAC3D,MAAM,KAAK,IAAIF,GAAME,CAAE;AAAA,EAChC;AAAA;AAAA,EAGA,KAA6BF,GAASE,GAA8C;AAClF,UAAMC,IAAM,KAAK,GAAGH,GAAM,CAACI,MAAY;AACrC,MAAAD,EAAA,GACAD,EAAGE,CAAO;AAAA,IACZ,CAAC;AACD,WAAOD;AAAA,EACT;AAAA,EAEA,IAA4BH,GAASE,GAAwC;AAC3E,UAAMG,IAAO,KAAK,UAAU,IAAIL,CAAI;AACpC,QAAI,CAACK,EAAM;AACX,UAAMC,IAAOD,EAAK,OAAO,CAACE,MAAMA,MAAML,CAAE;AACxC,IAAII,EAAK,SAAQ,KAAK,UAAU,IAAIN,GAAMM,CAAI,IACzC,KAAK,UAAU,OAAON,CAAI;AAAA,EACjC;AAAA,EAEA,KAA6BA,GAASI,GAA0B;AAE9D,eAAWF,KAAM,KAAK,UAAU,IAAIF,CAAI,KAAK;AAC3C,UAAI;AACD,QAAAE,EAAoCE,CAAO;AAAA,MAC9C,SAASH,GAAK;AACZ,aAAK,QAAQD,GAAMC,CAAG;AAAA,MACxB;AAAA,EAEJ;AAAA,EAEA,cAAcD,GAA4B;AACxC,WAAO,KAAK,UAAU,IAAIA,CAAI,GAAG,UAAU;AAAA,EAC7C;AAAA,EAEA,QAAc;AACZ,SAAK,UAAU,MAAA;AAAA,EACjB;AACF;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"FloatingText.js","sources":["../../src/fx/FloatingText.ts"],"sourcesContent":["import { type BitmapLabel, installLabelFont } from '../core/BitmapLabel';\nimport type { Container } from 'pixi.js';\nimport type { Stage } from '../core/Stage';\n\n/** One look of floating text: a size and a fill colour. Each look is its own baked font. */\nexport interface FloatingTextStyle {\n size: number;\n color: string;\n}\n\nexport interface FloatingTextOptions {\n /** A font already loaded (`document.fonts.load`), or the glyphs bake in the fallback. */\n fontFamily: string;\n /** Outline colour (default near-black). */\n stroke?: string;\n /** Outline width for a size (default a fifth of it, at least 2). */\n strokeWidth?: (size: number) => number;\n /** Rasterising resolution (default 4, crisp under a zoomed camera). */\n resolution?: number;\n /** Spent labels of one style kept for reuse; the rest are destroyed (default 24). */\n keepPerStyle?: number;\n /** Letters beyond ASCII the labels print (see `installLabelFont`), baked with each style's font. */\n glyphs?: string;\n /** Depth given to every label taken (default: left as made). */\n depth?: number;\n /**\n * Where new labels are added (default the stage's world layer). Give a scene's `root` and the labels live and die\n * with the scene: on the world layer they outlive it, and a label still floating when the scene changes stays.\n */\n parent?: Container;\n}\n\n/** How `float` moves a label: pops from `pop`× to 1, holds, then rises `rise` px while fading out. */\nexport interface FloatOptions {\n rise?: number;\n /** Sideways drift over the fade, px (pass a random one for a crowd of numbers that do not stack). */\n driftX?: number;\n pop?: number;\n popMs?: number;\n holdMs?: number;\n fadeMs?: number;\n}\n\nconst keyOf = (s: FloatingTextStyle) => `${s.size}|${s.color}`;\n\n/**\n * Floating text — damage numbers, pickups, \"+1\" — drawn from bitmap fonts and reused rather than created and\n * destroyed each time: a crowded fight throws dozens a second, and as canvas text each one repainted a canvas at\n * resolution 4 and uploaded it. A label keeps its font, so spent ones are pooled per style and only their string\n * changes. Fonts are baked ahead (`warm`): a style first met mid-fight baked its glyph pages on that frame.\n */\nexport class FloatingText {\n private readonly pools = new Map<string, BitmapLabel[]>();\n private readonly stroke: string;\n private readonly strokeWidth: (size: number) => number;\n private readonly resolution: number;\n private readonly keep: number;\n\n constructor(\n private readonly stage: Stage,\n private readonly options: FloatingTextOptions,\n ) {\n this.stroke = options.stroke ?? '#140e18';\n this.strokeWidth = options.strokeWidth ?? ((size) => Math.max(2, Math.round(size / 5)));\n this.resolution = options.resolution ?? 4;\n this.keep = options.keepPerStyle ?? 24;\n }\n\n /**\n * Bakes the fonts of `styles`, one a task starting `delayMs` from now, so no frame pays for them all at once — ten\n * at once froze a world's first frame half a second (each font measures and kerns every glyph). Returns a cancel.\n */\n warm(styles: readonly FloatingTextStyle[], delayMs = 1000, gapMs = 50): () => void {\n const todo = [...styles];\n let timer: ReturnType<typeof setTimeout> | undefined;\n const next = () => {\n const s = todo.shift();\n if (!s) return;\n this.install(s);\n timer = setTimeout(next, gapMs);\n };\n timer = setTimeout(next, delayMs);\n return () => clearTimeout(timer);\n }\n\n /** Bakes one style's font now. */\n install(style: FloatingTextStyle): void {\n installLabelFont(this.options.fontFamily, style.size, style.color, this.stroke, this.strokeWidth(style.size), this.resolution, this.options.glyphs);\n }\n\n /** A label ready to animate: shown, opaque, unscaled, anchored at its bottom centre on (x, y). */\n take(x: number, y: number, text: string, style: FloatingTextStyle): BitmapLabel {\n const pool = this.pools.get(keyOf(style));\n let t = pool?.pop();\n while (t && t.destroyed) t = pool?.pop();\n if (!t) {\n t = this.stage.add\n .label(x, y, text, this.options.fontFamily, style.size, style.color, this.stroke, this.strokeWidth(style.size), this.resolution, this.options.glyphs)\n .setOrigin(0.5, 1);\n // made on the world layer by the factory; moved under the game's own container when it named one\n if (this.options.parent) this.options.parent.addChild(t);\n } else {\n t.setText(text);\n t.position.set(x, y);\n t.alpha = 1;\n t.setScale(1);\n t.visible = true;\n }\n if (this.options.depth !== undefined) t.setDepth(this.options.depth);\n return t;\n }\n\n /** Hands a finished label back for reuse. */\n release(t: BitmapLabel, style: FloatingTextStyle): void {\n if (t.destroyed) return;\n const key = keyOf(style);\n let pool = this.pools.get(key);\n if (!pool) this.pools.set(key, (pool = []));\n if (pool.length >= this.keep) {\n t.destroy();\n return;\n }\n t.visible = false;\n pool.push(t);\n }\n\n /** A label that pops, rises, fades and goes back to the pool by itself, on the stage's own tweens. */\n float(x: number, y: number, text: string, style: FloatingTextStyle, options: FloatOptions = {}): BitmapLabel {\n const { rise = 16, driftX = 0, pop = 1.4, popMs = 120, holdMs = 350, fadeMs = 500 } = options;\n const t = this.take(x, y, text, style);\n const tweens = this.stage.tweens;\n if (pop !== 1) {\n t.setScale(pop);\n tweens.add({ targets: t, scale: 1, duration: popMs, ease: 'Back.Out' });\n }\n tweens.add({\n targets: t,\n x: x + driftX,\n y: y - rise,\n alpha: 0,\n delay: holdMs,\n duration: fadeMs,\n ease: 'Sine.In',\n onComplete: () => this.release(t, style),\n });\n return t;\n }\n\n /** Destroys every pooled label (a scene ending); labels still out are the scene's to free. */\n clear(): void {\n for (const pool of this.pools.values()) for (const t of pool) if (!t.destroyed) t.destroy();\n this.pools.clear();\n }\n}\n"],"names":["keyOf","s","FloatingText","stage","options","size","styles","delayMs","gapMs","todo","timer","next","style","installLabelFont","x","y","text","pool","t","key","rise","driftX","pop","popMs","holdMs","fadeMs","tweens"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"hitStop.js","sources":["../../src/fx/hitStop.ts"],"sourcesContent":["/*\n * How hard a landed blow feels. Pure, so the numbers\n * can be pinned: the freeze-frame grows with the share of the body's health the blow took, a crit holds longer, and the\n * body struck is knocked a pixel or three away from whoever struck it — less the bigger it is.\n */\n\n/** The freeze-frame of a blow (ms): 40 for a scratch, up to 140 for one that took a third of the body; a crit +40. */\nexport function hitStopMs(share: number, crit: boolean): number {\n const base = Math.min(140, 40 + Math.max(0, share) * 400);\n return Math.round(base + (crit ? 40 : 0));\n}\n\n/** How far the body struck recoils away from the blow (whole world px): a heavy body barely, a crit a pixel further. */\nexport function recoilPx(heavy: boolean, share: number, crit: boolean): number {\n if (heavy) return crit ? 2 : 1;\n return Math.min(3, 2 + (share > 0.2 ? 1 : 0) + (crit ? 1 : 0));\n}\n\n/** How long a recoil takes to settle back (ms). */\nexport const RECOIL_MS = 120;\n\n/** The recoil left at `t` (0 the blow, 1 settled): out at once, eased back, whole pixels only. */\nexport function recoilAt(px: number, t: number): number {\n if (t >= 1 || px <= 0) return 0;\n return Math.round(px * (1 - t) * (1 - t));\n}\n\n/** The least time between two white flashes on a heavy body (ms). */\nexport const HEAVY_FLASH_GAP_MS = 400;\n\n/**\n * How long the body struck flashes white (ms), 0 for none: 70, a crit of one's own 140 (the impact frame), nothing with\n * `reduce` (a \"reduce flashes\" setting). A heavy body (a boss, anything drawn large) flashes only for the local\n * player's own crit, 60 ms, `HEAVY_FLASH_GAP_MS` apart: a sheet covering most of the screen turning white on every\n * blow, beat and tick is a strobe. Sparks, numbers and the recoil still mark every blow.\n */\nexport function hurtFlashMs(heavy: boolean, impact: boolean, reduce: boolean): number {\n if (reduce) return 0;\n if (heavy) return impact ? 60 : 0;\n return impact ? 140 : 70;\n}\n\n/** The gap a body keeps between two flashes (ms). */\nexport function hurtFlashGapMs(heavy: boolean): number {\n return heavy ? HEAVY_FLASH_GAP_MS : 0;\n}\n"],"names":["hitStopMs","share","crit","base","recoilPx","heavy","RECOIL_MS","recoilAt","px","t","HEAVY_FLASH_GAP_MS","hurtFlashMs","impact","reduce","hurtFlashGapMs"],"mappings":"AAOO,SAASA,EAAUC,GAAeC,GAAuB;AAC9D,QAAMC,IAAO,KAAK,IAAI,KAAK,KAAK,KAAK,IAAI,GAAGF,CAAK,IAAI,GAAG;AACxD,SAAO,KAAK,MAAME,KAAQD,IAAO,KAAK,EAAE;AAC1C;AAGO,SAASE,EAASC,GAAgBJ,GAAeC,GAAuB;AAC7E,SAAIG,IAAcH,IAAO,IAAI,IACtB,KAAK,IAAI,GAAG,KAAKD,IAAQ,MAAM,IAAI,MAAMC,IAAO,IAAI,EAAE;AAC/D;AAGO,MAAMI,IAAY;AAGlB,SAASC,EAASC,GAAYC,GAAmB;AACtD,SAAIA,KAAK,KAAKD,KAAM,IAAU,IACvB,KAAK,MAAMA,KAAM,IAAIC,MAAM,IAAIA,EAAE;AAC1C;AAGO,MAAMC,IAAqB;AAQ3B,SAASC,EAAYN,GAAgBO,GAAiBC,GAAyB;AACpF,SAAIA,IAAe,IACfR,IAAcO,IAAS,KAAK,IACzBA,IAAS,MAAM;AACxB;AAGO,SAASE,EAAeT,GAAwB;AACrD,SAAOA,IAAQK,IAAqB;AACtC;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"viewCull.js","sources":["../../src/fx/viewCull.ts"],"sourcesContent":["/** What `inView` reads off the main camera (Camera's own fields; a fake stage may lack them). */\nexport interface CameraLike {\n zoom: number;\n scrollX?: number;\n scrollY?: number;\n width?: number;\n height?: number;\n}\n\n/**\n * Whether the world point (x, y) lies within `margin` world pixels of the screen. Effects use it to skip painting what\n * nobody can see. A camera that has no size yet (a tool's fake stage, before the first resize) counts everything in.\n */\nexport function inView(cam: CameraLike, x: number, y: number, margin: number): boolean {\n const w = cam.width;\n const h = cam.height;\n if (!w || !h || !(cam.zoom > 0)) return true;\n const left = cam.scrollX ?? 0;\n const top = cam.scrollY ?? 0;\n return x >= left - margin && y >= top - margin && x <= left + w / cam.zoom + margin && y <= top + h / cam.zoom + margin;\n}\n"],"names":["inView","cam","x","y","margin","w","h","left","top"],"mappings":"AAaO,SAASA,EAAOC,GAAiBC,GAAWC,GAAWC,GAAyB;AACrF,QAAMC,IAAIJ,EAAI,OACRK,IAAIL,EAAI;AACd,MAAI,CAACI,KAAK,CAACC,KAAK,EAAEL,EAAI,OAAO,GAAI,QAAO;AACxC,QAAMM,IAAON,EAAI,WAAW,GACtBO,IAAMP,EAAI,WAAW;AAC3B,SAAOC,KAAKK,IAAOH,KAAUD,KAAKK,IAAMJ,KAAUF,KAAKK,IAAOF,IAAIJ,EAAI,OAAOG,KAAUD,KAAKK,IAAMF,IAAIL,EAAI,OAAOG;AACnH;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"Translator.js","sources":["../../src/i18n/Translator.ts"],"sourcesContent":["import { formatMessage, sourceText } from './formatMessage';\n\n/** A translation that changes with a count, by the plural categories of the language (`one`, `few`, `many`, `other`…). */\nexport type PluralForms = Partial<Record<Intl.LDMLPluralRule, string>>;\n\n/** A language's translations, keyed by the English source (with its `|context`, if any). */\nexport type Catalog = Readonly<Record<string, string | PluralForms>>;\n\nexport interface TranslatorOptions {\n /** The language strings are written in (default 'en'): shown as written, never reported missing. */\n source?: string;\n /** The language to start in (default the source). */\n language?: string;\n /** Told once per key and language of every string with no translation, for tooling or a log. */\n onMissing?: (key: string, language: string) => void;\n}\n\n/**\n * Translation with the English source as the key: `tr('Open the gate')` shows the English until a catalog for the\n * current language has a line for it, so writing a new string never breaks the game, it only shows untranslated.\n *\n * - Placeholders are positional: `tr('{0} joined', name)`.\n * - The same English with two meanings gets a context after `|`: `tr('Close|window')`, `tr('Close|near')`. The\n * context is a separate key for translators and never shown.\n * - Counts: `trn('{0} coin', '{0} coins', n)` picks a form by `Intl.PluralRules`; the key is the plural form, and\n * a catalog may give every category its language has (`{ one, few, many, other }`). `{0}` is the count.\n * - `setLanguage` switches at once and tells subscribers, so a UI re-renders instead of reloading: read labels when\n * they are shown, never once at import.\n *\n * `tr` and `trn` are bound, so they can be handed around (`const { tr } = translator`).\n */\nexport class Translator {\n readonly source: string;\n private lang: string;\n private readonly catalogs = new Map<string, Catalog>();\n private readonly listeners = new Set<(language: string) => void>();\n private readonly rules = new Map<string, Intl.PluralRules>();\n private readonly reported = new Set<string>();\n private readonly onMissing: ((key: string, language: string) => void) | undefined;\n\n constructor(options: TranslatorOptions = {}) {\n this.source = options.source ?? 'en';\n this.lang = options.language ?? this.source;\n this.onMissing = options.onMissing;\n }\n\n get language(): string {\n return this.lang;\n }\n\n /** Languages with a catalog, plus the source. */\n get languages(): string[] {\n return [...new Set([this.source, ...this.catalogs.keys()])];\n }\n\n /** Adds (or merges into) a language's catalog; subscribers hear of it when it is the language shown. */\n addCatalog(language: string, catalog: Catalog): void {\n const had = this.catalogs.get(language);\n this.catalogs.set(language, had ? { ...had, ...catalog } : catalog);\n if (language === this.lang) this.notify();\n }\n\n setLanguage(language: string): void {\n if (language === this.lang) return;\n this.lang = language;\n this.notify();\n }\n\n /** Calls `fn` with the language after every switch; the returned function stops it. */\n subscribe(fn: (language: string) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n /** Whether the current language has its own line for `key`. */\n has(key: string): boolean {\n return this.catalogs.get(this.lang)?.[key] !== undefined;\n }\n\n readonly tr = (key: string, ...args: unknown[]): string => {\n const entry = this.lookup(key);\n let text: string;\n if (entry === undefined) text = sourceText(key);\n else if (typeof entry === 'string') text = entry;\n else text = entry.other ?? sourceText(key);\n return formatMessage(text, args);\n };\n\n readonly trn = (one: string, other: string, count: number, ...args: unknown[]): string => {\n const entry = this.lookup(other);\n const all = [count, ...args];\n if (entry === undefined) return formatMessage(sourceText(this.category(this.source, count) === 'one' ? one : other), all);\n if (typeof entry === 'string') return formatMessage(entry, all);\n const text = entry[this.category(this.lang, count)] ?? entry.other ?? sourceText(other);\n return formatMessage(text, all);\n };\n\n private lookup(key: string): string | PluralForms | undefined {\n const entry = this.catalogs.get(this.lang)?.[key];\n if (entry === undefined && this.lang !== this.source && this.onMissing) {\n const id = `${this.lang}\\u0000${key}`;\n if (!this.reported.has(id)) {\n this.reported.add(id);\n this.onMissing(key, this.lang);\n }\n }\n return entry;\n }\n\n private category(language: string, count: number): Intl.LDMLPluralRule {\n let r = this.rules.get(language);\n if (!r) {\n r = new Intl.PluralRules(language);\n this.rules.set(language, r);\n }\n return r.select(count);\n }\n\n private notify(): void {\n for (const fn of [...this.listeners]) {\n try {\n fn(this.lang);\n } catch (err) {\n console.error('[i18n] a language listener failed', err);\n }\n }\n }\n}\n"],"names":["Translator","options","language","catalog","had","fn","key","args","entry","text","sourceText","formatMessage","one","other","count","all","id","r","err"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"extractStrings.js","sources":["../../src/i18n/extractStrings.ts"],"sourcesContent":["/** One translatable string found in source text. */\nexport interface ExtractedString {\n /** The catalog key: the string as written, `|context` included. */\n key: string;\n /** For a plural call, the singular form written beside the key. */\n one?: string;\n /** The function it was passed to. */\n fn: string;\n /** 1-based line of the call. */\n line: number;\n}\n\nexport interface ExtractOptions {\n /** Functions whose first argument is a key (default `['tr']`). */\n single?: readonly string[];\n /** Functions called `(one, other, count…)`, keyed by `other` (default `['trn']`). */\n plural?: readonly string[];\n}\n\n/**\n * Finds the strings passed to `tr('…')` (and `trn('…', '…', n)`) in a file's text, for a tool that lists keys\n * missing from a catalog or drops unused ones. A plain scanner, not a parser: it reads a string literal straight\n * after the call's parenthesis (quotes of any kind, escapes decoded) and skips a template literal with `${…}` in it\n * or a call whose key is not a literal, since neither can be a catalog key. A call inside a comment is still found.\n */\nexport function extractStrings(text: string, options: ExtractOptions = {}): ExtractedString[] {\n const single = options.single ?? ['tr'];\n const plural = options.plural ?? ['trn'];\n const names = [...single, ...plural].map((n) => n.replace(/[$.*+?^()[\\]{}|\\\\]/g, '\\\\$&'));\n if (names.length === 0) return [];\n const call = new RegExp(`(?<![\\\\w$])(${names.join('|')})\\\\s*\\\\(`, 'g');\n const out: ExtractedString[] = [];\n let line = 1;\n let scanned = 0;\n for (const m of text.matchAll(call)) {\n const fn = m[1]!;\n const first = readString(text, m.index + m[0].length);\n if (!first) continue;\n for (; scanned < m.index; scanned++) if (text.charCodeAt(scanned) === 10) line++;\n if (!plural.includes(fn)) {\n out.push({ key: first.value, fn, line });\n continue;\n }\n let i = skipSpace(text, first.end);\n if (text[i] !== ',') continue;\n const second = readString(text, i + 1);\n if (second) out.push({ key: second.value, one: first.value, fn, line });\n }\n return out;\n}\n\nfunction skipSpace(text: string, i: number): number {\n while (i < text.length && /\\s/.test(text[i]!)) i++;\n return i;\n}\n\nconst ESCAPES: Record<string, string> = { n: '\\n', t: '\\t', r: '\\r', b: '\\b', f: '\\f', v: '\\v', '0': '\\0' };\n\n/** The string literal starting at `from` (after spaces), decoded, and the index after its closing quote. */\nfunction readString(text: string, from: number): { value: string; end: number } | null {\n const start = skipSpace(text, from);\n const quote = text[start];\n if (quote !== \"'\" && quote !== '\"' && quote !== '`') return null;\n let value = '';\n for (let i = start + 1; i < text.length; i++) {\n const c = text[i]!;\n if (c === quote) return { value, end: i + 1 };\n if (quote !== '`' && c === '\\n') return null;\n if (quote === '`' && c === '$' && text[i + 1] === '{') return null;\n if (c !== '\\\\') {\n value += c;\n continue;\n }\n const e = text[++i];\n if (e === undefined) return null;\n if (e === '\\n') continue; // a line continuation\n if (e === 'u' || e === 'x') {\n const braced = e === 'u' && text[i + 1] === '{';\n const hex = braced ? text.slice(i + 2, text.indexOf('}', i)) : text.slice(i + 1, i + (e === 'u' ? 5 : 3));\n const code = Number.parseInt(hex, 16);\n if (Number.isNaN(code)) return null;\n value += String.fromCodePoint(code);\n i += braced ? hex.length + 2 : hex.length;\n continue;\n }\n value += ESCAPES[e] ?? e;\n }\n return null;\n}\n"],"names":["extractStrings","text","options","single","plural","names","n","call","out","line","scanned","m","fn","first","readString","i","skipSpace","second","ESCAPES","from","start","quote","value","e","braced","hex","code"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"formatMessage.js","sources":["../../src/i18n/formatMessage.ts"],"sourcesContent":["/** The separator between a source string and its context: `'Open|door'` and `'Open|file'` are two keys. */\nexport const CONTEXT_MARK = '|';\n\n/** The text a key shows untranslated: the source without its `|context`. */\nexport function sourceText(key: string): string {\n const i = key.lastIndexOf(CONTEXT_MARK);\n return i < 0 ? key : key.slice(0, i);\n}\n\n/**\n * Fills `{0}`, `{1}`… with `args`. A placeholder with no argument is left as written, so a missing argument shows\n * where it is instead of printing \"undefined\". `{{` and `}}` print a literal brace.\n */\nexport function formatMessage(text: string, args: readonly unknown[]): string {\n if (text.indexOf('{') < 0 && text.indexOf('}') < 0) return text;\n return text.replace(/\\{\\{|\\}\\}|\\{(\\d+)\\}/g, (m, index: string | undefined) => {\n if (m === '{{') return '{';\n if (m === '}}') return '}';\n const i = Number(index);\n return i < args.length ? String(args[i]) : m;\n });\n}\n"],"names":["CONTEXT_MARK","sourceText","key","i","formatMessage","text","args","m","index"],"mappings":"AACO,MAAMA,IAAe;AAGrB,SAASC,EAAWC,GAAqB;AAC9C,QAAMC,IAAID,EAAI,YAAY,GAAY;AACtC,SAAOC,IAAI,IAAID,IAAMA,EAAI,MAAM,GAAGC,CAAC;AACrC;AAMO,SAASC,EAAcC,GAAcC,GAAkC;AAC5E,SAAID,EAAK,QAAQ,GAAG,IAAI,KAAKA,EAAK,QAAQ,GAAG,IAAI,IAAUA,IACpDA,EAAK,QAAQ,wBAAwB,CAACE,GAAGC,MAA8B;AAC5E,QAAID,MAAM,KAAM,QAAO;AACvB,QAAIA,MAAM,KAAM,QAAO;AACvB,UAAM,IAAI,OAAOC,CAAK;AACtB,WAAO,IAAIF,EAAK,SAAS,OAAOA,EAAK,CAAC,CAAC,IAAIC;AAAA,EAC7C,CAAC;AACH;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"Actions.js","sources":["../../src/input/Actions.ts"],"sourcesContent":["import type { Keyboard } from './Keyboard';\nimport { chordHeld, parseChord, type KeyChord } from './keyChord';\n\n/**\n * Named actions bound to any input: keys, gamepad buttons and sticks, and virtual buttons a touch UI presses. Game\n * code asks for `'jump'`, never for `Space`, so players can rebind and every device works the same.\n *\n * ```ts\n * const input = new Actions(keyboard, {\n * left: ['KeyA', 'ArrowLeft', 'pad:left', 'pad:lx-'],\n * right: ['KeyD', 'ArrowRight', 'pad:right', 'pad:lx+'],\n * jump: ['Space', 'pad:a'],\n * });\n * // in a fixed system:\n * input.poll();\n * if (input.pressed('jump')) …\n * const [x, y] = input.vector('left', 'right', 'up', 'down');\n * input.endStep();\n * ```\n *\n * Bindings: a `KeyboardEvent.code` (`KeyW`, `Space`, `ArrowUp`), a chord of modifiers and a code (`Shift+Tab`,\n * `Ctrl+KeyS`: `Shift`, `Ctrl`, `Alt`, `Meta`, either side), `pad:<button>` (standard mapping names below),\n * `pad:<axis>+` / `pad:<axis>-` for a stick half (`lx`, `ly`, `rx`, `ry`), or `touch:<name>` for a virtual button\n * (`press`/`release`).\n *\n * A chord wins over its bare key: while `Shift+Tab` is held, an action bound to plain `Tab` is not, and it stays off\n * until Tab itself comes up (letting Shift go first must not turn a Shift+Tab into a Tab). A bare key held with a\n * modifier no chord names still counts (Shift held to run does not stop the jump key).\n */\nexport type Bindings<A extends string> = Record<A, readonly string[]>;\n\n/** Button names of the browser's standard gamepad mapping, in index order. */\nexport const PAD_BUTTONS = ['a', 'b', 'x', 'y', 'lb', 'rb', 'lt', 'rt', 'select', 'start', 'ls', 'rs', 'up', 'down', 'left', 'right', 'home'] as const;\nconst PAD_AXES = ['lx', 'ly', 'rx', 'ry'] as const;\n\n/** What the actions read from a gamepad; `navigator.getGamepads()` entries satisfy it. */\nexport interface PadState {\n readonly buttons: readonly { readonly pressed: boolean; readonly value: number }[];\n readonly axes: readonly number[];\n}\n\nexport interface ActionsOptions {\n /** Stick values under this count as 0 (default 0.25). */\n deadZone?: number;\n /** Where gamepads come from (default `navigator.getGamepads`); tests pass their own. */\n gamepads?: () => readonly (PadState | null)[];\n}\n\nexport class Actions<A extends string> {\n private bindings: Map<A, readonly string[]>;\n private readonly virtual = new Set<string>();\n private readonly virtualDown = new Set<string>();\n /** Analog value of each action this step (0–1), and last step's, for edges. */\n private now = new Map<A, number>();\n private before = new Map<A, number>();\n private readonly deadZone: number;\n private readonly gamepads: () => readonly (PadState | null)[];\n /** Parsed chords by binding string; null for a plain key. */\n private readonly chords = new Map<string, KeyChord | null>();\n /** Keys a chord holds this step: their bare bindings read 0 until the key comes up. */\n private readonly claimed = new Set<string>();\n private readonly keyDown = (code: string) => this.keyboard !== null && (this.keyboard.isDown(code) || this.keyboard.pressed(code));\n\n constructor(\n private readonly keyboard: Keyboard | null,\n bindings: Bindings<A>,\n options: ActionsOptions = {},\n ) {\n this.bindings = new Map(Object.entries(bindings) as [A, readonly string[]][]);\n this.deadZone = options.deadZone ?? 0.25;\n this.gamepads =\n options.gamepads ?? (() => (typeof navigator !== 'undefined' && typeof navigator.getGamepads === 'function' ? navigator.getGamepads() : []));\n }\n\n /** Replaces one action's bindings (a player's rebinding). */\n bind(action: A, inputs: readonly string[]): void {\n this.bindings.set(action, [...inputs]);\n }\n\n bindingsOf(action: A): readonly string[] {\n return this.bindings.get(action) ?? [];\n }\n\n /** A touch UI's virtual button went down: `touch:<name>` bindings read it. */\n press(name: string): void {\n this.virtual.add(name);\n this.virtualDown.add(name);\n }\n\n release(name: string): void {\n this.virtual.delete(name);\n }\n\n /** Reads every device once; call at the start of each simulation step. */\n poll(): void {\n [this.before, this.now] = [this.now, this.before];\n this.now.clear();\n const pads = this.gamepads().filter((p): p is PadState => p !== null);\n this.claimChords();\n for (const [action, inputs] of this.bindings) {\n let v = 0;\n for (const input of inputs) v = Math.max(v, this.read(input, pads));\n if (v > 0) this.now.set(action, v);\n }\n }\n\n /** Held this step. */\n down(action: A): boolean {\n return (this.now.get(action) ?? 0) > 0;\n }\n\n /** How far it is held (0–1): 1 for a key, the trigger or stick amount for analog input. */\n value(action: A): number {\n return this.now.get(action) ?? 0;\n }\n\n /** Went down this step. */\n pressed(action: A): boolean {\n return this.down(action) && !((this.before.get(action) ?? 0) > 0);\n }\n\n /** Came up this step. */\n released(action: A): boolean {\n return !this.down(action) && (this.before.get(action) ?? 0) > 0;\n }\n\n /** `value(positive) - value(negative)`, in −1…1. */\n axis(negative: A, positive: A): number {\n return this.value(positive) - this.value(negative);\n }\n\n /** A 2D direction from four actions, never longer than 1 (a diagonal is not faster). */\n vector(left: A, right: A, up: A, down: A): [number, number] {\n const x = this.axis(left, right);\n const y = this.axis(up, down);\n const len = Math.hypot(x, y);\n return len > 1 ? [x / len, y / len] : [x, y];\n }\n\n /** Ends the step: the keyboard forgets its edges; virtual taps shorter than a step have now been seen. */\n endStep(): void {\n this.keyboard?.endStep();\n this.virtualDown.clear();\n }\n\n /**\n * Drops every pending edge: what went down or up since the last step is forgotten, and what is held now counts as\n * already held, so the next `poll` reports no `pressed` or `released` for it. For input that sat unread while\n * something else had the keys (a pause menu over the level; `SceneManager.pop` calls it through `Scene.useInput`).\n */\n flush(): void {\n this.keyboard?.endStep();\n this.virtualDown.clear();\n this.poll();\n this.before = new Map(this.now);\n }\n\n private chordOf(input: string): KeyChord | null {\n let c = this.chords.get(input);\n if (c === undefined) {\n c = input.startsWith('pad:') || input.startsWith('touch:') ? null : parseChord(input);\n this.chords.set(input, c);\n }\n return c;\n }\n\n /** Marks the keys held chords use, and frees those whose key has come up. */\n private claimChords(): void {\n for (const code of this.claimed) if (!this.keyDown(code)) this.claimed.delete(code);\n if (!this.keyboard) return;\n for (const inputs of this.bindings.values())\n for (const input of inputs) {\n const chord = this.chordOf(input);\n if (chord && chordHeld(chord, this.keyDown)) this.claimed.add(chord.code);\n }\n }\n\n private read(input: string, pads: readonly PadState[]): number {\n if (input.startsWith('touch:')) {\n const name = input.slice(6);\n return this.virtual.has(name) || this.virtualDown.has(name) ? 1 : 0;\n }\n if (input.startsWith('pad:')) {\n const what = input.slice(4);\n let v = 0;\n const sign = what.endsWith('+') ? 1 : what.endsWith('-') ? -1 : 0;\n if (sign !== 0) {\n const axis = PAD_AXES.indexOf(what.slice(0, -1) as (typeof PAD_AXES)[number]);\n if (axis < 0) return 0;\n for (const p of pads) {\n const raw = (p.axes[axis] ?? 0) * sign;\n if (raw > this.deadZone) v = Math.max(v, (raw - this.deadZone) / (1 - this.deadZone));\n }\n return Math.min(1, v);\n }\n const button = PAD_BUTTONS.indexOf(what as (typeof PAD_BUTTONS)[number]);\n if (button < 0) return 0;\n // a digital button reports value 1 when pressed; an analog trigger its depth (some pads report 0 for a press)\n for (const p of pads) {\n const b = p.buttons[button];\n if (b?.pressed) v = Math.max(v, b.value > 0 ? b.value : 1);\n }\n return Math.min(1, v);\n }\n const chord = this.chordOf(input);\n if (chord) return chordHeld(chord, this.keyDown) ? 1 : 0;\n if (this.claimed.has(input)) return 0;\n return this.keyDown(input) ? 1 : 0;\n }\n}\n"],"names":["PAD_BUTTONS","PAD_AXES","Actions","keyboard","bindings","options","code","action","inputs","name","pads","p","v","input","negative","positive","left","right","up","down","x","y","len","c","parseChord","chord","chordHeld","what","sign","axis","raw","button","b"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"AimGesture.js","sources":["../../src/input/AimGesture.ts"],"sourcesContent":["/**\n * The drag-to-aim gesture of a touch skill button (the MOBA way), kept pure so it can be tested. Pressing an aimed\n * button opens a circle round it with a knob under the thumb; where the knob sits in that circle is where the skill\n * goes. A press that never wanders past the tap slop is a tap (the game casts it the way a tap always does: at the\n * locked target, the nearest enemy, the facing). Letting go over a cancel zone casts nothing; letting go with the knob\n * back on the button casts as a tap, or cancels when `centreCancels` (the one way to cancel when there is no zone).\n *\n * The thumb gets a longer slop in the first `pressMs` of a press: a hard press slides as it lands, and a quick tap\n * that slid past the plain slop became a drag that flew wherever the slide pointed.\n *\n * ```ts\n * const g = new AimGesture({ radius: 90, centreCancels: !hasCancelZone });\n * // pointermove: g.move(dx, dy, overCancelZone, now - downAt)\n * // pointerup: const end = g.up(); // 'tap' | 'cast' | 'cancel'\n * // while g.aiming, draw the aim from aimPoint(caster, shape, g.share, facing)\n * ```\n */\n\nexport type AimEnd = 'tap' | 'cast' | 'cancel';\n\nexport interface AimGestureOptions {\n /** The circle's radius, in the same units as the thumb's offsets (CSS px). */\n radius: number;\n /** Scales the thumb's travel into the knob's (a sensitivity setting); the slops stay the thumb's own (default 1). */\n sensitivity?: number;\n /** The knob let go back on the button cancels rather than casting as a tap (default true). */\n centreCancels?: boolean;\n /** How far the thumb may wander before a press is a drag (default 10). */\n tapSlop?: number;\n /** The slop in a press's first `pressMs` (default 28). */\n earlySlop?: number;\n /** How long a press is \"landing\" (default 180 ms). */\n pressMs?: number;\n /** Inside this share of the radius the knob is back on the button (default 0.2). */\n centre?: number;\n}\n\nexport const AIM_TAP_SLOP = 10;\nexport const AIM_EARLY_SLOP = 28;\nexport const AIM_PRESS_MS = 180;\nexport const AIM_CENTRE = 0.2;\n\n/**\n * How far the thumb must wander, `ms` into a press, before it drags. `early` false keeps the plain slop throughout\n * (an attack button with nothing locked: there a slide is as good as a drag).\n */\nexport function dragSlop(ms: number, early = true, tapSlop = AIM_TAP_SLOP, earlySlop = AIM_EARLY_SLOP, pressMs = AIM_PRESS_MS): number {\n return early && ms < pressMs ? earlySlop : tapSlop;\n}\n\ntype Point = { x: number; y: number };\n\nexport class AimGesture {\n private dragged = false;\n private over = false;\n /** Where the knob is drawn, clamped to the circle, relative to the button's centre. */\n knob: Point = { x: 0, y: 0 };\n /** The knob as a vector of length ≤ 1: what the world reads. */\n share: Point = { x: 0, y: 0 };\n private readonly radius: number;\n private readonly sensitivity: number;\n private readonly centreCancels: boolean;\n private readonly tapSlop: number;\n private readonly earlySlop: number;\n private readonly pressMs: number;\n private readonly centre: number;\n\n constructor(options: AimGestureOptions) {\n this.radius = Math.max(1e-6, options.radius);\n this.sensitivity = options.sensitivity ?? 1;\n this.centreCancels = options.centreCancels ?? true;\n this.tapSlop = options.tapSlop ?? AIM_TAP_SLOP;\n this.earlySlop = options.earlySlop ?? AIM_EARLY_SLOP;\n this.pressMs = options.pressMs ?? AIM_PRESS_MS;\n this.centre = options.centre ?? AIM_CENTRE;\n }\n\n /**\n * The thumb at `rawX, rawY` from the button's centre, `overCancel` when it is over the cancel zone, `ms` since it\n * went down (Infinity skips the landing slop). Once a drag, always a drag for this press.\n */\n move(rawX: number, rawY: number, overCancel = false, ms = Infinity): void {\n if (Math.hypot(rawX, rawY) > dragSlop(ms, true, this.tapSlop, this.earlySlop, this.pressMs)) this.dragged = true;\n this.over = overCancel;\n const dx = rawX * this.sensitivity;\n const dy = rawY * this.sensitivity;\n const len = Math.hypot(dx, dy);\n const k = len > this.radius ? this.radius / len : 1;\n this.knob = { x: dx * k, y: dy * k };\n this.share = { x: this.knob.x / this.radius, y: this.knob.y / this.radius };\n }\n\n /** A drag has begun: show the aim from now on. */\n get aiming(): boolean {\n return this.dragged;\n }\n\n private get centred(): boolean {\n return Math.hypot(this.share.x, this.share.y) < this.centre;\n }\n\n /** Letting go now would cast nothing (draw the cancel zone lit). */\n get cancelling(): boolean {\n return this.dragged && (this.over || (this.centreCancels && this.centred));\n }\n\n up(): AimEnd {\n if (!this.dragged) return 'tap';\n if (this.cancelling) return 'cancel';\n return this.centred ? 'tap' : 'cast';\n }\n}\n\n/**\n * How a skill is aimed: at a spot within `range` (a blast, a blink), or along a direction for `length` (a shot, a\n * dash; `backwards` for a leap away from the aim).\n */\nexport type AimShape =\n | { kind: 'point'; range: number }\n | { kind: 'direction'; length: number; backwards?: boolean; snap?: number };\n\n/**\n * How far out a spot skill's knob puts it, as a share of its range: from the centre ring's edge to the rim is the\n * whole reach, so a knob just out of the ring lands at the caster's feet — with `share × range`, nothing nearer than\n * the ring could ever be reached.\n */\nexport function spotReach(len: number, centre = AIM_CENTRE): number {\n return Math.max(0, Math.min(1, (len - centre) / (1 - centre)));\n}\n\n/** The knob that puts a spot skill `dx, dy` from the caster: `spotReach`'s inverse (aim assist, replays). */\nexport function spotShare(dx: number, dy: number, range: number, centre = AIM_CENTRE): Point {\n const d = Math.hypot(dx, dy);\n if (d < 1e-6) return { x: 0, y: 0 };\n const len = centre + (1 - centre) * Math.min(1, d / range);\n return { x: (dx / d) * len, y: (dy / d) * len };\n}\n\n/**\n * Where an aimed skill goes, as a mouse would have said it: the aim angle and the ground point. A knob with no length\n * keeps `lastAim`. A direction may snap to `snap` radians (0 or none for free aim).\n */\nexport function aimPoint(from: Point, shape: AimShape, share: Point, lastAim: number, centre = AIM_CENTRE): { aim: number; x: number; y: number } {\n const len = Math.hypot(share.x, share.y);\n const angle = len > 1e-6 ? Math.atan2(share.y, share.x) : lastAim;\n if (shape.kind === 'point') {\n const d = spotReach(len, centre) * shape.range;\n return { aim: angle, x: from.x + Math.cos(angle) * d, y: from.y + Math.sin(angle) * d };\n }\n const snapped = shape.snap ? Math.round(angle / shape.snap) * shape.snap : angle;\n const aim = shape.backwards ? snapped + Math.PI : snapped;\n return { aim, x: from.x + Math.cos(aim) * shape.length, y: from.y + Math.sin(aim) * shape.length };\n}\n"],"names":["AIM_TAP_SLOP","AIM_EARLY_SLOP","AIM_PRESS_MS","AIM_CENTRE","dragSlop","ms","early","tapSlop","earlySlop","pressMs","AimGesture","options","rawX","rawY","overCancel","dx","dy","len","k","spotReach","centre","spotShare","range","d","aimPoint","from","shape","share","lastAim","angle","snapped","aim"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"Keyboard.js","sources":["../../src/input/Keyboard.ts"],"sourcesContent":["/**\n * Which keys are held, by `KeyboardEvent.code` (the physical key, so WASD stays WASD on any layout). `pressed` and\n * `released` report edges since the last `endStep`; call that once per simulation step so a tap shorter than a frame\n * is still seen by exactly one step.\n *\n * It only knows keys pressed **after** it was made: a key already held when it is created (the Enter that started a\n * level, held into it) is not down here until it is pressed again. A scene that makes its own keyboard in a field\n * therefore starts with every key up, which is usually what a new screen wants; share one keyboard across scenes when\n * a held key must carry over. Modifier keys are ordinary codes here (`ShiftLeft`); `Actions` reads chords from them.\n */\nexport class Keyboard {\n private readonly held = new Set<string>();\n private readonly down = new Set<string>();\n private readonly up = new Set<string>();\n private readonly cleanup: (() => void)[] = [];\n\n constructor(target: Window | HTMLElement = window) {\n const keydown = (e: Event) => {\n const k = (e as KeyboardEvent).code;\n if ((e as KeyboardEvent).repeat || this.held.has(k)) return;\n this.held.add(k);\n this.down.add(k);\n };\n const keyup = (e: Event) => {\n const k = (e as KeyboardEvent).code;\n if (!this.held.delete(k)) return;\n this.up.add(k);\n };\n // a key held while the window loses focus never sends its keyup: let everything go\n const blur = () => {\n for (const k of this.held) this.up.add(k);\n this.held.clear();\n };\n target.addEventListener('keydown', keydown);\n target.addEventListener('keyup', keyup);\n window.addEventListener('blur', blur);\n this.cleanup.push(\n () => target.removeEventListener('keydown', keydown),\n () => target.removeEventListener('keyup', keyup),\n () => window.removeEventListener('blur', blur),\n );\n }\n\n isDown(code: string): boolean {\n return this.held.has(code);\n }\n\n /** Any of these keys is held. */\n anyDown(...codes: string[]): boolean {\n return codes.some((c) => this.held.has(c));\n }\n\n /** Went down since the last `endStep`. */\n pressed(code: string): boolean {\n return this.down.has(code);\n }\n\n /** Came up since the last `endStep`. */\n released(code: string): boolean {\n return this.up.has(code);\n }\n\n /** -1, 0 or 1 from two sets of keys: `axis(['KeyA', 'ArrowLeft'], ['KeyD', 'ArrowRight'])`. */\n axis(negative: string[], positive: string[]): number {\n return (this.anyDown(...positive) ? 1 : 0) - (this.anyDown(...negative) ? 1 : 0);\n }\n\n /** Forgets this step's edges; held keys stay held. */\n endStep(): void {\n this.down.clear();\n this.up.clear();\n }\n\n destroy(): void {\n for (const fn of this.cleanup.splice(0)) fn();\n this.held.clear();\n this.endStep();\n }\n}\n"],"names":["Keyboard","target","keydown","e","k","keyup","blur","code","codes","c","negative","positive","fn"],"mappings":"AAUO,MAAMA,EAAS;AAAA,EACH,2BAAW,IAAA;AAAA,EACX,2BAAW,IAAA;AAAA,EACX,yBAAS,IAAA;AAAA,EACT,UAA0B,CAAA;AAAA,EAE3C,YAAYC,IAA+B,QAAQ;AACjD,UAAMC,IAAU,CAACC,MAAa;AAC5B,YAAMC,IAAKD,EAAoB;AAC/B,MAAKA,EAAoB,UAAU,KAAK,KAAK,IAAIC,CAAC,MAClD,KAAK,KAAK,IAAIA,CAAC,GACf,KAAK,KAAK,IAAIA,CAAC;AAAA,IACjB,GACMC,IAAQ,CAACF,MAAa;AAC1B,YAAMC,IAAKD,EAAoB;AAC/B,MAAK,KAAK,KAAK,OAAOC,CAAC,KACvB,KAAK,GAAG,IAAIA,CAAC;AAAA,IACf,GAEME,IAAO,MAAM;AACjB,iBAAWF,KAAK,KAAK,KAAM,MAAK,GAAG,IAAIA,CAAC;AACxC,WAAK,KAAK,MAAA;AAAA,IACZ;AACA,IAAAH,EAAO,iBAAiB,WAAWC,CAAO,GAC1CD,EAAO,iBAAiB,SAASI,CAAK,GACtC,OAAO,iBAAiB,QAAQC,CAAI,GACpC,KAAK,QAAQ;AAAA,MACX,MAAML,EAAO,oBAAoB,WAAWC,CAAO;AAAA,MACnD,MAAMD,EAAO,oBAAoB,SAASI,CAAK;AAAA,MAC/C,MAAM,OAAO,oBAAoB,QAAQC,CAAI;AAAA,IAAA;AAAA,EAEjD;AAAA,EAEA,OAAOC,GAAuB;AAC5B,WAAO,KAAK,KAAK,IAAIA,CAAI;AAAA,EAC3B;AAAA;AAAA,EAGA,WAAWC,GAA0B;AACnC,WAAOA,EAAM,KAAK,CAACC,MAAM,KAAK,KAAK,IAAIA,CAAC,CAAC;AAAA,EAC3C;AAAA;AAAA,EAGA,QAAQF,GAAuB;AAC7B,WAAO,KAAK,KAAK,IAAIA,CAAI;AAAA,EAC3B;AAAA;AAAA,EAGA,SAASA,GAAuB;AAC9B,WAAO,KAAK,GAAG,IAAIA,CAAI;AAAA,EACzB;AAAA;AAAA,EAGA,KAAKG,GAAoBC,GAA4B;AACnD,YAAQ,KAAK,QAAQ,GAAGA,CAAQ,IAAI,IAAI,MAAM,KAAK,QAAQ,GAAGD,CAAQ,IAAI,IAAI;AAAA,EAChF;AAAA;AAAA,EAGA,UAAgB;AACd,SAAK,KAAK,MAAA,GACV,KAAK,GAAG,MAAA;AAAA,EACV;AAAA,EAEA,UAAgB;AACd,eAAWE,KAAM,KAAK,QAAQ,OAAO,CAAC,EAAG,CAAAA,EAAA;AACzC,SAAK,KAAK,MAAA,GACV,KAAK,QAAA;AAAA,EACP;AACF;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"Keymap.js","sources":["../../src/input/Keymap.ts"],"sourcesContent":["import type { Actions } from './Actions';\n\n/**\n * Which keys each action sits on, as the player has rebound them: one table the input, the key hints on screen and\n * a settings screen all read. A binding is a `KeyboardEvent.code` (`KeyW`, `Digit1`), so it means the same physical\n * key on every layout. Each action keeps a list (W and ArrowUp, 1 and Q); a settings screen edits the **first** and\n * leaves the alternates alone.\n *\n * Two rules: one key serves one action at a time (a key bound twice would fire both, and the player could not tell\n * which they meant), and reserved keys (Escape, Enter, Tab by default: the interface's own) are never taken.\n *\n * Inputs the player cannot rebind — the gamepad's, a touch button, a reserved key an action should still answer to\n * (Escape also pausing) — are the keymap's **fixed** inputs: given once to the constructor, laid after the player's\n * keys by `applyTo`, and untouched by either rule above.\n *\n * ```ts\n * const keys = new Keymap({ jump: ['Space'], left: ['KeyA', 'ArrowLeft'], pause: ['KeyP'] }, undefined, { jump: ['pad:a'], pause: ['Escape'] });\n * keys.load(keymapStorage('my-game.keys').read());\n * keys.onChange(() => { keys.applyTo(actions); storage.write(keys.toJSON()); });\n * keys.bind('pause', 'KeyA'); // 'left' keeps only ArrowLeft\n * ```\n */\nexport class Keymap<A extends string> {\n private binds: Record<A, string[]>;\n private readonly defaults: Record<A, readonly string[]>;\n private readonly reserved: ReadonlySet<string>;\n private readonly fixed: Partial<Record<A, readonly string[]>>;\n private readonly listeners: (() => void)[] = [];\n\n /**\n * `reserved`: keys never bound (default Escape, Enter, Tab; pass `undefined` for the default). `fixed`: inputs each\n * action always has besides the player's keys (gamepad, touch, a reserved key), never saved or rebound.\n */\n constructor(\n defaults: Readonly<Record<A, readonly string[]>>,\n reserved: Iterable<string> = ['Escape', 'Enter', 'Tab'],\n fixed: Partial<Record<A, readonly string[]>> = {},\n ) {\n this.defaults = { ...defaults };\n this.reserved = new Set(reserved);\n this.fixed = { ...fixed };\n this.binds = this.fresh();\n }\n\n private fresh(): Record<A, string[]> {\n const out = {} as Record<A, string[]>;\n for (const id of this.actions()) out[id] = this.defaults[id].filter((c) => !this.reserved.has(c));\n return out;\n }\n\n /** Every action, in the order the defaults named them. */\n actions(): A[] {\n return Object.keys(this.defaults) as A[];\n }\n\n isReserved(code: string): boolean {\n return this.reserved.has(code);\n }\n\n /** The keys of one action; the first is the one a settings screen shows and edits. */\n keysFor(action: A): readonly string[] {\n return this.binds[action] ?? [];\n }\n\n /** The inputs an action always has besides the player's keys (the constructor's `fixed`). */\n fixedFor(action: A): readonly string[] {\n return this.fixed[action] ?? [];\n }\n\n /** Which action a key belongs to, or undefined. */\n actionFor(code: string): A | undefined {\n for (const id of this.actions()) if (this.binds[id].includes(code)) return id;\n return undefined;\n }\n\n /**\n * Puts `code` first on `action`, taking it off whatever else held it; returns the actions that lost it (to tell the\n * player). A reserved key is refused (returns null). An action stripped of its last key keeps none: it is unbound\n * until something is put back on it.\n */\n bind(action: A, code: string): A[] | null {\n if (this.reserved.has(code) || !(action in this.binds)) return null;\n const lost: A[] = [];\n for (const id of this.actions()) {\n if (id === action || !this.binds[id].includes(code)) continue;\n this.binds[id] = this.binds[id].filter((c) => c !== code);\n lost.push(id);\n }\n const rest = this.binds[action].slice(1).filter((c) => c !== code);\n this.binds[action] = [code, ...rest];\n this.changed();\n return lost;\n }\n\n /** Takes every key off `action`. */\n unbind(action: A): void {\n if (!this.binds[action]?.length) return;\n this.binds[action] = [];\n this.changed();\n }\n\n /** Every action back on its defaults. */\n reset(): void {\n this.binds = this.fresh();\n this.changed();\n }\n\n /** A plain object to store wherever the game keeps its settings. */\n toJSON(): Record<A, string[]> {\n const out = {} as Record<A, string[]>;\n for (const id of this.actions()) out[id] = [...this.binds[id]];\n return out;\n }\n\n /**\n * A saved set laid over this build's defaults. Only actions this build knows are read, so an old save cannot leave\n * a key bound to nothing; an action newer than the save keeps its defaults only where no saved action holds that key\n * (the saved one is what the player chose). Anything malformed is ignored.\n */\n load(saved: unknown): void {\n this.binds = mergeKeymap(this.defaults, saved, this.reserved);\n this.changed();\n }\n\n /**\n * Hands the bindings to an `Actions`: each action's keys, then its fixed inputs, then its `extra` ones. Only the\n * keymap's actions are bound, so the `Actions` may hold more (menu actions alongside the rebindable ones, each on\n * its own fixed inputs); those are left as they are. Fixed and extra inputs are the game's, so neither the reserved\n * keys nor one-key-one-action apply to them.\n */\n applyTo<B extends string>(actions: Actions<A | B>, extra: Partial<Record<A, readonly string[]>> = {}): void {\n for (const id of this.actions()) actions.bind(id, [...this.binds[id], ...(this.fixed[id] ?? []), ...(extra[id] ?? [])]);\n }\n\n /** Called after every change; returns a function that stops it. */\n onChange(fn: () => void): () => void {\n this.listeners.push(fn);\n return () => {\n const i = this.listeners.indexOf(fn);\n if (i >= 0) this.listeners.splice(i, 1);\n };\n }\n\n private changed(): void {\n for (const fn of [...this.listeners]) fn();\n }\n}\n\n/** `saved` laid over `defaults` by `Keymap.load`'s rules, as a fresh object. */\nexport function mergeKeymap<A extends string>(\n defaults: Readonly<Record<A, readonly string[]>>,\n saved: unknown,\n reserved: ReadonlySet<string> = new Set(),\n): Record<A, string[]> {\n const ids = Object.keys(defaults) as A[];\n const out = {} as Record<A, string[]>;\n const kept = new Set<A>();\n const s: Partial<Record<A, unknown>> = saved && typeof saved === 'object' ? (saved as Partial<Record<A, unknown>>) : {};\n for (const id of ids) {\n const codes = s[id];\n if (Array.isArray(codes) && codes.every((c) => typeof c === 'string')) {\n // a saved empty list is a choice too: the player unbound it\n out[id] = [...new Set(codes as string[])].filter((c) => !reserved.has(c));\n kept.add(id);\n } else out[id] = defaults[id].filter((c) => !reserved.has(c));\n }\n // one key one action, even in a hand-edited save: the first action listed keeps it\n const owner = new Map<string, A>();\n for (const id of ids)\n if (kept.has(id))\n out[id] = out[id].filter((c) => {\n if (owner.has(c)) return false;\n owner.set(c, id);\n return true;\n });\n for (const id of ids) if (!kept.has(id)) out[id] = out[id].filter((c) => !owner.has(c));\n return out;\n}\n\n/** A place to keep a keymap in the browser, under a key the game names; every failure falls back quietly. */\nexport function keymapStorage(key: string): { read(): unknown; write(binds: object): void; clear(): void } {\n const store = (): Storage | null => {\n try {\n return typeof localStorage === 'undefined' ? null : localStorage;\n } catch {\n return null;\n }\n };\n return {\n read() {\n try {\n const raw = store()?.getItem(key);\n return raw ? (JSON.parse(raw) as unknown) : null;\n } catch {\n // unreadable or blocked storage: the defaults are a complete set on their own\n return null;\n }\n },\n write(binds) {\n try {\n store()?.setItem(key, JSON.stringify(binds));\n } catch {\n // storage unavailable: the change lasts until the page is reloaded\n }\n },\n clear() {\n try {\n store()?.removeItem(key);\n } catch {\n // nothing to clear\n }\n },\n };\n}\n\nconst KEY_NAMES: Readonly<Record<string, string>> = {\n Space: 'Space',\n ShiftLeft: 'Shift',\n ShiftRight: 'Right Shift',\n ControlLeft: 'Ctrl',\n ControlRight: 'Right Ctrl',\n AltLeft: 'Alt',\n AltRight: 'Right Alt',\n ArrowUp: 'Up',\n ArrowDown: 'Down',\n ArrowLeft: 'Left',\n ArrowRight: 'Right',\n Backquote: '`',\n Minus: '-',\n Equal: '=',\n Semicolon: ';',\n Quote: \"'\",\n Comma: ',',\n Period: '.',\n Slash: '/',\n Backslash: '\\\\',\n BracketLeft: '[',\n BracketRight: ']',\n};\n\n/**\n * How a key code reads on screen: \"KeyW\" is W, \"Digit1\" is 1, \"Numpad3\" is \"Num 3\", a chord \"Shift+Tab\" each part in\n * turn. `names` overrides any code or modifier (a game's own language for the arrows and modifiers).\n */\nexport function keyLabel(code: string, names: Readonly<Record<string, string>> = {}): string {\n if (code.length > 1 && code.includes('+')) return code.split('+').map((part) => keyLabel(part, names)).join('+');\n const own = names[code];\n if (own !== undefined) return own;\n if (code.startsWith('Key')) return code.slice(3);\n if (code.startsWith('Digit')) return code.slice(5);\n if (code.startsWith('Numpad')) return `Num ${code.slice(6)}`;\n return KEY_NAMES[code] ?? code;\n}\n"],"names":["Keymap","defaults","reserved","fixed","out","id","c","code","action","lost","rest","saved","mergeKeymap","actions","extra","fn","i","ids","kept","s","codes","owner","keymapStorage","key","store","raw","binds","KEY_NAMES","keyLabel","names","part","own"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"TargetLock.js","sources":["../../src/input/TargetLock.ts"],"sourcesContent":["/**\n * Picking and holding a target on a touch screen, kept pure so it can be tested. Dragging an attack button points:\n * the body lying closest to that direction is picked (and of two about as close to it, the nearer). A target button\n * picks the nearest. The lock then holds — every attack, skill and the facing goes at it, even while a stick walks\n * the body elsewhere — until that body is gone or leaves `keepRange`; a later drag that points at nothing keeps it.\n *\n * Bodies come from a callback as `{ id, x, y }`: the game decides what may be locked (monsters, a duel's opponent).\n */\n\nexport interface LockBody {\n id: number;\n x: number;\n y: number;\n}\n\ntype Point = { x: number; y: number };\n\nexport const LOCK_PICK_RANGE = 220;\nexport const LOCK_KEEP_RANGE = 300;\n/** Widest angle off the drag a body may stand and still be picked: 50° each side. */\nexport const LOCK_PICK_CONE = (50 * Math.PI) / 180;\n\n/** The signed difference a − b, wrapped into −π…π. */\nfunction angleDiff(a: number, b: number): number {\n let d = (a - b) % (Math.PI * 2);\n if (d > Math.PI) d -= Math.PI * 2;\n if (d < -Math.PI) d += Math.PI * 2;\n return d;\n}\n\n/**\n * The body a drag in direction `dir` (any length > 0) picks from `from`, or null. Scored by how far off the drag's\n * line it stands, with distance as a lighter second weight, so a far body dead ahead beats a near one off to the side.\n */\nexport function pickTarget(from: Point, dir: Point, bodies: readonly LockBody[], range = LOCK_PICK_RANGE, cone = LOCK_PICK_CONE): number | null {\n const len = Math.hypot(dir.x, dir.y);\n if (len < 1e-6) return null;\n const heading = Math.atan2(dir.y, dir.x);\n let best: number | null = null;\n let bestScore = Infinity;\n for (const b of bodies) {\n const d = Math.hypot(b.x - from.x, b.y - from.y);\n if (d > range) continue;\n // a body right on top of the player is in every direction\n const off = d < 1 ? 0 : Math.abs(angleDiff(Math.atan2(b.y - from.y, b.x - from.x), heading));\n if (off > cone) continue;\n const score = off / cone + (0.35 * d) / range;\n if (score < bestScore) {\n bestScore = score;\n best = b.id;\n }\n }\n return best;\n}\n\n/** The nearest body within `range` (ties to the lower id, so two clients pick alike), or null. */\nexport function nearestTarget(from: Point, bodies: readonly LockBody[], range = LOCK_PICK_RANGE): number | null {\n let best: number | null = null;\n let bestD = Infinity;\n for (const b of bodies) {\n const d = Math.hypot(b.x - from.x, b.y - from.y);\n if (d > range) continue;\n if (d < bestD || (d === bestD && best !== null && b.id < best)) {\n bestD = d;\n best = b.id;\n }\n }\n return best;\n}\n\n/** The locked body while the lock still holds (present, within `range`), else null. */\nexport function keptTarget<T extends LockBody>(from: Point, locked: number | null, bodies: readonly T[], range = LOCK_KEEP_RANGE): T | null {\n if (locked === null) return null;\n const b = bodies.find((c) => c.id === locked);\n if (!b || Math.hypot(b.x - from.x, b.y - from.y) > range) return null;\n return b;\n}\n\nexport interface TargetLockOptions {\n pickRange?: number;\n keepRange?: number;\n /** Half-angle (radians) a drag picks within. */\n cone?: number;\n}\n\n/** The lock as state: which body is held, picked by a drag or the nearest, let go when it is gone or too far. */\nexport class TargetLock<T extends LockBody = LockBody> {\n private id: number | null = null;\n private readonly pickRange: number;\n private readonly keepRange: number;\n private readonly cone: number;\n\n constructor(\n private readonly bodies: () => readonly T[],\n options: TargetLockOptions = {},\n ) {\n this.pickRange = options.pickRange ?? LOCK_PICK_RANGE;\n this.keepRange = options.keepRange ?? LOCK_KEEP_RANGE;\n this.cone = options.cone ?? LOCK_PICK_CONE;\n }\n\n /** The id held, without checking it still stands (use `current` for that). */\n get locked(): number | null {\n return this.id;\n }\n\n /** A drag in `dir` from `from`: locks what it points at; pointing at nothing keeps the old lock. */\n pick(from: Point, dir: Point): number | null {\n const id = pickTarget(from, dir, this.bodies(), this.pickRange, this.cone);\n if (id !== null) this.id = id;\n return this.id;\n }\n\n /** Locks the nearest body, every call, whatever was held. */\n pickNearest(from: Point): number | null {\n this.id = nearestTarget(from, this.bodies(), this.pickRange);\n return this.id;\n }\n\n /** The held body if it still holds; a lock that no longer does is dropped. */\n current(from: Point): T | null {\n const b = keptTarget(from, this.id, this.bodies(), this.keepRange);\n if (!b) this.id = null;\n return b;\n }\n\n clear(): void {\n this.id = null;\n }\n}\n"],"names":["LOCK_PICK_RANGE","LOCK_KEEP_RANGE","LOCK_PICK_CONE","angleDiff","a","b","d","pickTarget","from","dir","bodies","range","cone","heading","best","bestScore","off","score","nearestTarget","bestD","keptTarget","locked","TargetLock","options","id"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"aimAxis.js","sources":["../../src/input/aimAxis.ts"],"sourcesContent":["/**\n * An aim turned into the eight-way axis the keyboard makes (each part -1, 0 or 1; `stepMover` scales a diagonal),\n * so a step sent along it is one a key could have sent. A part counts once the aim leans past 22.5° towards it.\n */\nexport function aimAxis(aim: number): { dx: number; dy: number } {\n const c = Math.cos(aim);\n const s = Math.sin(aim);\n const lean = Math.sin(Math.PI / 8);\n return { dx: Math.abs(c) > lean ? Math.sign(c) : 0, dy: Math.abs(s) > lean ? Math.sign(s) : 0 };\n}\n"],"names":["aimAxis","aim","c","s","lean"],"mappings":"AAIO,SAASA,EAAQC,GAAyC;AAC/D,QAAMC,IAAI,KAAK,IAAID,CAAG,GAChBE,IAAI,KAAK,IAAIF,CAAG,GAChBG,IAAO,KAAK,IAAI,KAAK,KAAK,CAAC;AACjC,SAAO,EAAE,IAAI,KAAK,IAAIF,CAAC,IAAIE,IAAO,KAAK,KAAKF,CAAC,IAAI,GAAG,IAAI,KAAK,IAAIC,CAAC,IAAIC,IAAO,KAAK,KAAKD,CAAC,IAAI,EAAA;AAC9F;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"keyChord.js","sources":["../../src/input/keyChord.ts"],"sourcesContent":["/**\n * A key binding with modifiers: `Shift+Tab`, `Ctrl+KeyS`, `Ctrl+Shift+KeyZ`. The last part is the key's\n * `KeyboardEvent.code`; before it, any of `Shift`, `Ctrl`, `Alt`, `Meta`, either side of the keyboard counting.\n * A chord matches while its key and every modifier it names are held; modifiers it does not name are not looked at.\n */\nexport interface KeyChord {\n code: string;\n /** The modifiers' key codes, each a pair: either one held counts. */\n modifiers: readonly (readonly [string, string])[];\n}\n\nconst MODIFIERS: Readonly<Record<string, readonly [string, string]>> = {\n Shift: ['ShiftLeft', 'ShiftRight'],\n Ctrl: ['ControlLeft', 'ControlRight'],\n Alt: ['AltLeft', 'AltRight'],\n Meta: ['MetaLeft', 'MetaRight'],\n};\n\n/** The chord a binding names, or null for a plain key (no `+`) or a malformed one (an unknown modifier). */\nexport function parseChord(binding: string): KeyChord | null {\n const parts = binding.split('+');\n if (parts.length < 2) return null;\n const code = parts[parts.length - 1]!;\n if (!code) return null;\n const modifiers: (readonly [string, string])[] = [];\n for (const name of parts.slice(0, -1)) {\n const pair = MODIFIERS[name];\n if (!pair) return null;\n modifiers.push(pair);\n }\n return { code, modifiers };\n}\n\n/** Whether `chord` is held, given which keys are down. */\nexport function chordHeld(chord: KeyChord, down: (code: string) => boolean): boolean {\n if (!down(chord.code)) return false;\n for (const [left, right] of chord.modifiers) if (!down(left) && !down(right)) return false;\n return true;\n}\n"],"names":["MODIFIERS","parseChord","binding","parts","code","modifiers","name","pair","chordHeld","chord","down","left","right"],"mappings":"AAWA,MAAMA,IAAiE;AAAA,EACrE,OAAO,CAAC,aAAa,YAAY;AAAA,EACjC,MAAM,CAAC,eAAe,cAAc;AAAA,EACpC,KAAK,CAAC,WAAW,UAAU;AAAA,EAC3B,MAAM,CAAC,YAAY,WAAW;AAChC;AAGO,SAASC,EAAWC,GAAkC;AAC3D,QAAMC,IAAQD,EAAQ,MAAM,GAAG;AAC/B,MAAIC,EAAM,SAAS,EAAG,QAAO;AAC7B,QAAMC,IAAOD,EAAMA,EAAM,SAAS,CAAC;AACnC,MAAI,CAACC,EAAM,QAAO;AAClB,QAAMC,IAA2C,CAAA;AACjD,aAAWC,KAAQH,EAAM,MAAM,GAAG,EAAE,GAAG;AACrC,UAAMI,IAAOP,EAAUM,CAAI;AAC3B,QAAI,CAACC,EAAM,QAAO;AAClB,IAAAF,EAAU,KAAKE,CAAI;AAAA,EACrB;AACA,SAAO,EAAE,MAAAH,GAAM,WAAAC,EAAA;AACjB;AAGO,SAASG,EAAUC,GAAiBC,GAA0C;AACnF,MAAI,CAACA,EAAKD,EAAM,IAAI,EAAG,QAAO;AAC9B,aAAW,CAACE,GAAMC,CAAK,KAAKH,EAAM,UAAW,KAAI,CAACC,EAAKC,CAAI,KAAK,CAACD,EAAKE,CAAK,EAAG,QAAO;AACrF,SAAO;AACT;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"longPress.js","sources":["../../src/input/longPress.ts"],"sourcesContent":["/**\n * A long press, as pure timing so it can be tested: a finger that stays down `ms` without moving more than `slop`\n * CSS px fires once, where it went down. A second finger, a move past the slop or a lift before the time is up is\n * not a long press — a tap stays a tap. The caller owns the clock (`fire(now)` from a timer or a frame).\n */\nexport class LongPress {\n private finger: { id: number; x: number; y: number; at: number } | null = null;\n private done = false;\n\n constructor(\n readonly ms = 480,\n readonly slop = 10,\n ) {}\n\n down(id: number, x: number, y: number, now: number): void {\n // a second finger means a pinch or the other thumb, never a press on one body\n if (this.finger) {\n this.finger = null;\n this.done = true;\n return;\n }\n this.finger = { id, x, y, at: now };\n this.done = false;\n }\n\n move(id: number, x: number, y: number): void {\n const f = this.finger;\n if (f && f.id === id && Math.hypot(x - f.x, y - f.y) > this.slop) this.finger = null;\n }\n\n up(id: number): void {\n if (this.finger?.id === id) this.finger = null;\n if (!this.finger) this.done = false;\n }\n\n /** Where the press is, the first time this is asked at or after `ms`; null otherwise. */\n fire(now: number): { x: number; y: number } | null {\n const f = this.finger;\n if (!f || this.done || now - f.at < this.ms) return null;\n this.done = true;\n return { x: f.x, y: f.y };\n }\n}\n"],"names":["LongPress","ms","slop","id","x","y","now","f"],"mappings":"AAKO,MAAMA,EAAU;AAAA,EAIrB,YACWC,IAAK,KACLC,IAAO,IAChB;AAFS,SAAA,KAAAD,GACA,KAAA,OAAAC;AAAA,EACR;AAAA,EAFQ;AAAA,EACA;AAAA,EALH,SAAkE;AAAA,EAClE,OAAO;AAAA,EAOf,KAAKC,GAAYC,GAAWC,GAAWC,GAAmB;AAExD,QAAI,KAAK,QAAQ;AACf,WAAK,SAAS,MACd,KAAK,OAAO;AACZ;AAAA,IACF;AACA,SAAK,SAAS,EAAE,IAAAH,GAAI,GAAAC,GAAG,GAAAC,GAAG,IAAIC,EAAA,GAC9B,KAAK,OAAO;AAAA,EACd;AAAA,EAEA,KAAKH,GAAYC,GAAWC,GAAiB;AAC3C,UAAME,IAAI,KAAK;AACf,IAAIA,KAAKA,EAAE,OAAOJ,KAAM,KAAK,MAAMC,IAAIG,EAAE,GAAGF,IAAIE,EAAE,CAAC,IAAI,KAAK,cAAW,SAAS;AAAA,EAClF;AAAA,EAEA,GAAGJ,GAAkB;AACnB,IAAI,KAAK,QAAQ,OAAOA,WAAS,SAAS,OACrC,KAAK,WAAQ,KAAK,OAAO;AAAA,EAChC;AAAA;AAAA,EAGA,KAAKG,GAA8C;AACjD,UAAMC,IAAI,KAAK;AACf,WAAI,CAACA,KAAK,KAAK,QAAQD,IAAMC,EAAE,KAAK,KAAK,KAAW,QACpD,KAAK,OAAO,IACL,EAAE,GAAGA,EAAE,GAAG,GAAGA,EAAE,EAAA;AAAA,EACxB;AACF;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"stickAxis.js","sources":["../../src/input/stickAxis.ts"],"sourcesContent":["/**\n * The virtual joystick's arithmetic, kept pure so it can be tested. The stick hands the game the **same** axis\n * the keyboard does — `stepMover` clamps every step to -1/0/1 on each axis (movement is eight-way, and the server\n * reads it the same way), so an analog stick would only be rounded there anyway. The rounding happens here instead,\n * by eight 45° sectors, so a thumb held a little off the horizontal walks straight rather than flickering between\n * east and north-east.\n */\n\n/** Below this share of the stick's radius the thumb is resting: no movement, and the aim falls back (touchAim.ts). */\nexport const STICK_DEAD_ZONE = 0.28;\n\nexport interface StickVector {\n x: number;\n y: number;\n}\n\n/** The thumb's offset from where the stick was planted, as a vector of length at most 1 (radius = full tilt). */\nexport function stickVector(dx: number, dy: number, radius: number): StickVector {\n if (radius <= 0) return { x: 0, y: 0 };\n const len = Math.hypot(dx, dy);\n if (len === 0) return { x: 0, y: 0 };\n const k = len > radius ? 1 / len : 1 / radius;\n return { x: dx * k, y: dy * k };\n}\n\n/** True when the stick is tilted past the dead zone. */\nexport function stickActive(v: StickVector, deadZone = STICK_DEAD_ZONE): boolean {\n return Math.hypot(v.x, v.y) >= deadZone;\n}\n\n/**\n * The keyboard-shaped axis for a stick vector: each of dx, dy is -1, 0 or 1 (screen y grows downward, as the\n * keyboard's \"down\" does), nothing inside the dead zone, and the nearest of the eight directions otherwise.\n */\nexport function stickToAxis(v: StickVector, deadZone = STICK_DEAD_ZONE): { dx: number; dy: number } {\n if (!stickActive(v, deadZone)) return { dx: 0, dy: 0 };\n const sector = Math.round(Math.atan2(v.y, v.x) / (Math.PI / 4));\n const angle = sector * (Math.PI / 4);\n const round = (n: number) => (Math.abs(n) < 1e-9 ? 0 : Math.sign(n));\n return { dx: round(Math.cos(angle)), dy: round(Math.sin(angle)) };\n}\n"],"names":["STICK_DEAD_ZONE","stickVector","dx","dy","radius","len","k","stickActive","v","deadZone","stickToAxis","angle","round","n"],"mappings":"AASO,MAAMA,IAAkB;AAQxB,SAASC,EAAYC,GAAYC,GAAYC,GAA6B;AAC/E,MAAIA,KAAU,EAAG,QAAO,EAAE,GAAG,GAAG,GAAG,EAAA;AACnC,QAAMC,IAAM,KAAK,MAAMH,GAAIC,CAAE;AAC7B,MAAIE,MAAQ,EAAG,QAAO,EAAE,GAAG,GAAG,GAAG,EAAA;AACjC,QAAMC,IAAID,IAAMD,IAAS,IAAIC,IAAM,IAAID;AACvC,SAAO,EAAE,GAAGF,IAAKI,GAAG,GAAGH,IAAKG,EAAA;AAC9B;AAGO,SAASC,EAAYC,GAAgBC,IAAW,MAA0B;AAC/E,SAAO,KAAK,MAAMD,EAAE,GAAGA,EAAE,CAAC,KAAKC;AACjC;AAMO,SAASC,EAAYF,GAAgBC,IAAW,MAA6C;AAClG,MAAI,CAACF,EAAYC,GAAGC,CAAQ,UAAU,EAAE,IAAI,GAAG,IAAI,EAAA;AAEnD,QAAME,IADS,KAAK,MAAM,KAAK,MAAMH,EAAE,GAAGA,EAAE,CAAC,KAAK,KAAK,KAAK,EAAE,KACtC,KAAK,KAAK,IAC5BI,IAAQ,CAACC,MAAe,KAAK,IAAIA,CAAC,IAAI,OAAO,IAAI,KAAK,KAAKA,CAAC;AAClE,SAAO,EAAE,IAAID,EAAM,KAAK,IAAID,CAAK,CAAC,GAAG,IAAIC,EAAM,KAAK,IAAID,CAAK,CAAC,EAAA;AAChE;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"touchDetect.js","sources":["../../src/input/touchDetect.ts"],"sourcesContent":["/** What decides whether the touch controls are shown; read off the browser by `isTouchUi` (`platform/touch.ts`). */\nexport interface TouchEnv {\n /** Running inside a native shell (`setNativeShell`: Capacitor, a kiosk…). */\n native: boolean;\n /** `(pointer: coarse)` matches: the **primary** pointer is a finger. */\n coarse: boolean;\n /** `navigator.maxTouchPoints`. */\n touchPoints: number;\n /** `?touch=1|0` in the URL or `localStorage` (`setTouchOverrideKey`), for trying the controls on a desktop browser. */\n override: string | null;\n}\n\n/**\n * Touch controls only where a finger is the main way in: the native app always, a browser whose primary pointer\n * is coarse and that reports touch points (a phone or tablet), never a desktop — a laptop with a touch screen\n * still has a fine primary pointer and keeps its mouse and keyboard.\n */\nexport function detectTouch(env: TouchEnv): boolean {\n if (env.override === '1' || env.override === 'on') return true;\n if (env.override === '0' || env.override === 'off') return false;\n if (env.native) return true;\n return env.coarse && env.touchPoints > 0;\n}\n"],"names":["detectTouch","env"],"mappings":"AAiBO,SAASA,EAAYC,GAAwB;AAClD,SAAIA,EAAI,aAAa,OAAOA,EAAI,aAAa,OAAa,KACtDA,EAAI,aAAa,OAAOA,EAAI,aAAa,QAAc,KACvDA,EAAI,SAAe,KAChBA,EAAI,UAAUA,EAAI,cAAc;AACzC;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"LightLayer.js","sources":["../../src/light/LightLayer.ts"],"sourcesContent":["import { Container, RenderTexture, Sprite, Texture } from 'pixi.js';\nimport type { Stage } from '../core/Stage';\n\n/** A light in the world. Change its fields any time (follow a body by writing `x`/`y` each frame). */\nexport interface Light {\n x: number;\n y: number;\n /** World px. */\n radius: number;\n /** The colour it casts on what it lights (default warm white). */\n color?: number;\n /** 0–1: how much of the dark it takes away, and how strong its colour (default 1). */\n intensity?: number;\n /** Radius wobble, 0–1 (a torch ≈ 0.08). */\n flicker?: number;\n /** A light off is kept but not drawn. */\n on?: boolean;\n}\n\nexport interface LightLayerOptions {\n /** Colour of the dark (default deep blue). */\n ambient?: number;\n /** 0 no dark at all, 1 black where nothing is lit (default 0.85). */\n darkness?: number;\n /** How strongly lights tint what they light, 0–1 (default 0.35). */\n tint?: number;\n /** A light's height over its width: the ground is seen at a slant (default 1, round; a 3/4 view ≈ 0.85). */\n squash?: number;\n}\n\nlet gradient: Texture | null = null;\n\n/** One soft radial falloff, white with alpha, shared by every light. */\nfunction falloff(): Texture {\n if (gradient) return gradient;\n const size = 256;\n const canvas = document.createElement('canvas');\n canvas.width = canvas.height = size;\n const ctx = canvas.getContext('2d');\n if (!ctx) return Texture.EMPTY;\n const g = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);\n // a bright core and a long, eased tail: reads as light, not as a disc\n g.addColorStop(0, 'rgba(255,255,255,1)');\n g.addColorStop(0.35, 'rgba(255,255,255,0.75)');\n g.addColorStop(0.7, 'rgba(255,255,255,0.25)');\n g.addColorStop(1, 'rgba(255,255,255,0)');\n ctx.fillStyle = g;\n ctx.fillRect(0, 0, size, size);\n gradient = Texture.from(canvas);\n gradient.source.scaleMode = 'linear';\n return gradient;\n}\n\n/**\n * Smooth darkness with lights cut out of it, for art that is not pixel art (the pixel module has its own dithered\n * `Darkness`). The dark is a screen-sized texture: each light erases a soft hole in it, and lays its colour over what\n * shows through. It sits on the screen layer, over the world and under the HUD you add after it.\n *\n * ```ts\n * const night = new LightLayer(stage, { darkness: 0.9 });\n * const torch = night.add({ x: 100, y: 80, radius: 90, color: 0xffb060, flicker: 0.08 });\n * // each frame: torch.x = hero.x; night.update(now);\n * ```\n */\nexport class LightLayer {\n private readonly lights = new Set<Light>();\n private readonly scratch = new Container();\n private readonly sheet = new Sprite(Texture.WHITE);\n private readonly holes = new Container();\n private readonly washes = new Container();\n private readonly view: Sprite;\n private rt: RenderTexture;\n private readonly pool: { hole: Sprite; wash: Sprite }[] = [];\n private ambient: number;\n private darkness: number;\n private readonly tint: number;\n private readonly squash: number;\n private readonly unsubscribe: () => void;\n\n constructor(\n private readonly stage: Stage,\n options: LightLayerOptions = {},\n ) {\n this.ambient = options.ambient ?? 0x0a0c24;\n this.darkness = options.darkness ?? 0.85;\n this.tint = options.tint ?? 0.35;\n this.squash = options.squash ?? 1;\n this.scratch.addChild(this.sheet, this.holes);\n this.rt = RenderTexture.create({ width: 1, height: 1 });\n this.view = new Sprite(this.rt);\n // the colour washes sit over the dark, added to what they light\n this.washes.blendMode = 'add';\n stage.screen.addChild(this.view, this.washes);\n this.unsubscribe = stage.onResize(() => this.fit());\n this.fit();\n }\n\n add(light: Light): Light {\n this.lights.add(light);\n return light;\n }\n\n remove(light: Light): void {\n this.lights.delete(light);\n }\n\n clear(): void {\n this.lights.clear();\n }\n\n /** Changes the dark: its colour and how dark (a day/night cycle eases these). */\n setAmbient(color: number, darkness: number): void {\n this.ambient = color;\n this.darkness = Math.min(1, Math.max(0, darkness));\n }\n\n /** Redraws the dark with this frame's lights. Call once a frame, after the camera moved. */\n update(now = performance.now()): void {\n const on = this.darkness > 0.004;\n this.view.visible = on;\n this.washes.visible = on;\n if (!on) return;\n const cam = this.stage.cameras.main;\n const z = cam.zoom;\n this.sheet.tint = this.ambient;\n this.sheet.alpha = this.darkness;\n let used = 0;\n for (const l of this.lights) {\n if (l.on === false || l.radius <= 0) continue;\n const sx = (l.x - cam.scrollX) * z;\n const sy = (l.y - cam.scrollY) * z;\n const wobble = l.flicker ? 1 + Math.sin(now / 97 + l.x) * Math.sin(now / 53 + l.y) * l.flicker : 1;\n const r = l.radius * z * wobble;\n // off screen by more than its radius: nothing of it shows\n if (sx < -r || sy < -r || sx > cam.width + r || sy > cam.height + r) continue;\n const s = this.sprites(used++);\n const intensity = Math.min(1, Math.max(0, l.intensity ?? 1));\n for (const sprite of [s.hole, s.wash]) {\n sprite.visible = true;\n sprite.position.set(sx, sy);\n sprite.width = r * 2;\n sprite.height = r * 2 * this.squash;\n }\n s.hole.alpha = intensity;\n s.wash.tint = l.color ?? 0xfff0dc;\n s.wash.alpha = intensity * this.tint;\n }\n for (let i = used; i < this.pool.length; i++) {\n this.pool[i]!.hole.visible = false;\n this.pool[i]!.wash.visible = false;\n }\n this.stage.app.renderer.render({ container: this.scratch, target: this.rt, clear: true });\n }\n\n destroy(): void {\n this.unsubscribe();\n this.view.destroy();\n this.washes.destroy({ children: true });\n this.scratch.destroy({ children: true });\n this.rt.destroy(true);\n this.lights.clear();\n }\n\n private sprites(i: number): { hole: Sprite; wash: Sprite } {\n let s = this.pool[i];\n if (!s) {\n const hole = new Sprite(falloff());\n hole.anchor.set(0.5);\n hole.blendMode = 'erase';\n const wash = new Sprite(falloff());\n wash.anchor.set(0.5);\n this.holes.addChild(hole);\n this.washes.addChild(wash);\n s = { hole, wash };\n this.pool.push(s);\n }\n return s;\n }\n\n private fit(): void {\n const w = Math.max(1, Math.ceil(this.stage.width));\n const h = Math.max(1, Math.ceil(this.stage.height));\n this.rt.resize(w, h);\n this.sheet.width = w;\n this.sheet.height = h;\n }\n}\n"],"names":["gradient","falloff","size","canvas","ctx","Texture","g","LightLayer","stage","options","RenderTexture","Sprite","Container","light","color","darkness","now","on","cam","z","used","l","sx","sy","wobble","s","intensity","sprite","i","hole","wash","w","h"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"MinHeap.js","sources":["../../src/nav/MinHeap.ts"],"sourcesContent":["/**\n * A binary min-heap of (index, priority) pairs, kept as two parallel arrays so a search over a big grid pushes\n * numbers, not objects. A stale entry (pushed again at a lower priority) is left in and skipped by the caller's\n * closed set, which is cheaper than a decrease-key.\n */\nexport class MinHeap {\n private items: number[] = [];\n private prio: number[] = [];\n\n get size(): number {\n return this.items.length;\n }\n\n push(index: number, priority: number): void {\n const a = this.items;\n const p = this.prio;\n a.push(index);\n p.push(priority);\n let c = a.length - 1;\n while (c > 0) {\n const u = (c - 1) >> 1;\n if (p[u]! <= p[c]!) break;\n [a[u], a[c]] = [a[c]!, a[u]!];\n [p[u], p[c]] = [p[c]!, p[u]!];\n c = u;\n }\n }\n\n /** The index with the lowest priority, removed. Only call while `size > 0`. */\n pop(): number {\n const a = this.items;\n const p = this.prio;\n const top = a[0]!;\n const lastI = a.pop()!;\n const lastF = p.pop()!;\n if (a.length > 0) {\n a[0] = lastI;\n p[0] = lastF;\n let u = 0;\n for (;;) {\n const l = u * 2 + 1;\n const r = l + 1;\n let s = u;\n if (l < a.length && p[l]! < p[s]!) s = l;\n if (r < a.length && p[r]! < p[s]!) s = r;\n if (s === u) break;\n [a[u], a[s]] = [a[s]!, a[u]!];\n [p[u], p[s]] = [p[s]!, p[u]!];\n u = s;\n }\n }\n return top;\n }\n\n clear(): void {\n this.items.length = 0;\n this.prio.length = 0;\n }\n}\n"],"names":["MinHeap","index","priority","a","p","c","u","top","lastI","lastF","l","r","s"],"mappings":"AAKO,MAAMA,EAAQ;AAAA,EACX,QAAkB,CAAA;AAAA,EAClB,OAAiB,CAAA;AAAA,EAEzB,IAAI,OAAe;AACjB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,KAAKC,GAAeC,GAAwB;AAC1C,UAAMC,IAAI,KAAK,OACTC,IAAI,KAAK;AACf,IAAAD,EAAE,KAAKF,CAAK,GACZG,EAAE,KAAKF,CAAQ;AACf,QAAIG,IAAIF,EAAE,SAAS;AACnB,WAAOE,IAAI,KAAG;AACZ,YAAMC,IAAKD,IAAI,KAAM;AACrB,UAAID,EAAEE,CAAC,KAAMF,EAAEC,CAAC,EAAI;AACpB,OAACF,EAAEG,CAAC,GAAGH,EAAEE,CAAC,CAAC,IAAI,CAACF,EAAEE,CAAC,GAAIF,EAAEG,CAAC,CAAE,GAC5B,CAACF,EAAEE,CAAC,GAAGF,EAAEC,CAAC,CAAC,IAAI,CAACD,EAAEC,CAAC,GAAID,EAAEE,CAAC,CAAE,GAC5BD,IAAIC;AAAA,IACN;AAAA,EACF;AAAA;AAAA,EAGA,MAAc;AACZ,UAAMH,IAAI,KAAK,OACTC,IAAI,KAAK,MACTG,IAAMJ,EAAE,CAAC,GACTK,IAAQL,EAAE,IAAA,GACVM,IAAQL,EAAE,IAAA;AAChB,QAAID,EAAE,SAAS,GAAG;AAChB,MAAAA,EAAE,CAAC,IAAIK,GACPJ,EAAE,CAAC,IAAIK;AACP,UAAIH,IAAI;AACR,iBAAS;AACP,cAAMI,IAAIJ,IAAI,IAAI,GACZK,IAAID,IAAI;AACd,YAAIE,IAAIN;AAGR,YAFII,IAAIP,EAAE,UAAUC,EAAEM,CAAC,IAAKN,EAAEQ,CAAC,MAAIA,IAAIF,IACnCC,IAAIR,EAAE,UAAUC,EAAEO,CAAC,IAAKP,EAAEQ,CAAC,MAAIA,IAAID,IACnCC,MAAMN,EAAG;AACb,SAACH,EAAEG,CAAC,GAAGH,EAAES,CAAC,CAAC,IAAI,CAACT,EAAES,CAAC,GAAIT,EAAEG,CAAC,CAAE,GAC5B,CAACF,EAAEE,CAAC,GAAGF,EAAEQ,CAAC,CAAC,IAAI,CAACR,EAAEQ,CAAC,GAAIR,EAAEE,CAAC,CAAE,GAC5BA,IAAIM;AAAA,MACN;AAAA,IACF;AACA,WAAOL;AAAA,EACT;AAAA,EAEA,QAAc;AACZ,SAAK,MAAM,SAAS,GACpB,KAAK,KAAK,SAAS;AAAA,EACrB;AACF;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"findPath.js","sources":["../../src/nav/findPath.ts"],"sourcesContent":["import { MinHeap } from './MinHeap';\nimport { canStep, isOpen, NAV_STEPS, type NavGrid, type NavOptions, type TilePoint } from './navGrid';\nimport { nearestOpen } from './nearestOpen';\n\n/**\n * Where a path ends. A point goal is met anywhere within `reach` tiles of its tile (centre to centre; 0 = that tile\n * only): aiming at an exact tile sent auto-walks into a spot inside a hut and left them shuffling against its wall a\n * step from where they were already close enough. A custom goal names its own tiles, with an `estimate` of the\n * tiles left that must never be more than the real cost (0, the default, makes the search a plain Dijkstra).\n */\nexport type NavGoal = { x: number; y: number; reach?: number } | { test: (tx: number, ty: number) => boolean; estimate?: (tx: number, ty: number) => number };\n\nexport interface FindPathOptions extends NavOptions {\n /** Tiles settled before giving up, so a goal walled off on a big map cannot stall a frame. */\n maxNodes?: number;\n}\n\n/** Octile distance: the cost of the cheapest eight-way walk over open ground. */\nexport function octile(dx: number, dy: number): number {\n const ax = Math.abs(dx);\n const ay = Math.abs(dy);\n return ax + ay + (Math.SQRT2 - 2) * Math.min(ax, ay);\n}\n\nfunction goalOf(goal: NavGoal): { test: (x: number, y: number) => boolean; estimate: (x: number, y: number) => number } {\n if ('test' in goal) return { test: goal.test, estimate: goal.estimate ?? (() => 0) };\n const r = goal.reach ?? 0;\n return {\n test: (x, y) => Math.hypot(x - goal.x, y - goal.y) <= r + 1e-9,\n // less the goal's own radius: never more than the real remaining cost\n estimate: (x, y) => Math.max(0, octile(goal.x - x, goal.y - y) - r),\n };\n}\n\n/**\n * A* over the grid: the tiles from `from` to the goal, both included, or null when there is no way through. A start\n * on a wall is moved to the nearest open tile. The path is the cheapest by `cost`, but among equally cheap ones it\n * is whichever the heap settled on — often a staircase; `straightenPath` irons that out.\n */\nexport function findPath(g: NavGrid, from: TilePoint, to: NavGoal, options: FindPathOptions = {}): TilePoint[] | null {\n const { cost, endOnly, maxNodes = 14_000 } = options;\n const start = isOpen(g, from.x, from.y) ? from : nearestOpen(g, from.x, from.y);\n if (!start) return null;\n const goal = goalOf(to);\n if (goal.test(start.x, start.y)) return [{ x: start.x, y: start.y }];\n\n const W = g.width;\n const n = W * g.height;\n const best = new Float64Array(n).fill(Infinity);\n const cameFrom = new Int32Array(n).fill(-1);\n const closed = new Uint8Array(n);\n const heap = new MinHeap();\n const s = start.y * W + start.x;\n best[s] = 0;\n heap.push(s, goal.estimate(start.x, start.y));\n let visited = 0;\n while (heap.size > 0 && visited < maxNodes) {\n const i = heap.pop();\n if (closed[i]) continue;\n closed[i] = 1;\n visited++;\n const cx = i % W;\n const cy = (i / W) | 0;\n if (goal.test(cx, cy)) {\n const out: TilePoint[] = [];\n for (let k = i; k !== -1; k = cameFrom[k]!) out.push({ x: k % W, y: (k / W) | 0 });\n return out.reverse();\n }\n for (const [dx, dy, step] of NAV_STEPS) {\n if (!canStep(g, cx, cy, dx, dy, endOnly, goal.test)) continue;\n const nx = cx + dx;\n const ny = cy + dy;\n const ni = ny * W + nx;\n if (closed[ni]) continue;\n const tentative = best[i]! + step + (cost ? cost(nx, ny) : 0);\n if (tentative >= best[ni]!) continue;\n best[ni] = tentative;\n cameFrom[ni] = i;\n heap.push(ni, tentative + goal.estimate(nx, ny));\n }\n }\n return null;\n}\n\n/**\n * Walking cost from one tile to every tile of the grid (row-major, `Infinity` where unreachable), by the same steps\n * and costs `findPath` takes — for weighing several destinations by real distance rather than by a straight line. An\n * end-only tile gets a cost (it can be stepped onto) but nothing is reached through it. `maxCost` stops the spread.\n */\nexport function floodFill(g: NavGrid, from: TilePoint, options: NavOptions & { maxCost?: number } = {}): Float32Array {\n const { cost, endOnly, maxCost = Infinity } = options;\n const W = g.width;\n const n = W * g.height;\n const out = new Float32Array(n).fill(Infinity);\n const start = isOpen(g, from.x, from.y) ? from : nearestOpen(g, from.x, from.y);\n if (!start) return out;\n const closed = new Uint8Array(n);\n const heap = new MinHeap();\n const s = start.y * W + start.x;\n out[s] = 0;\n heap.push(s, 0);\n // every end-only tile counts as a goal here, so it is reached; the check below keeps it from being passed through\n const always = () => true;\n while (heap.size > 0) {\n const i = heap.pop();\n if (closed[i]) continue;\n closed[i] = 1;\n const cx = i % W;\n const cy = (i / W) | 0;\n if (i !== s && endOnly?.(cx, cy)) continue;\n for (const [dx, dy, step] of NAV_STEPS) {\n if (!canStep(g, cx, cy, dx, dy, endOnly, always)) continue;\n // a diagonal may still not brush past an end-only tile\n if (endOnly && dx !== 0 && dy !== 0 && (endOnly(cx + dx, cy) || endOnly(cx, cy + dy))) continue;\n const nx = cx + dx;\n const ny = cy + dy;\n const ni = ny * W + nx;\n if (closed[ni]) continue;\n const c = out[i]! + step + (cost ? cost(nx, ny) : 0);\n if (c >= out[ni]! || c > maxCost) continue;\n out[ni] = c;\n heap.push(ni, c);\n }\n }\n return out;\n}\n\n/** How many tiles a flood reached: a quick test that a change to the ground walled nothing off. */\nexport function reachedCount(costs: Float32Array): number {\n let n = 0;\n for (let i = 0; i < costs.length; i++) if (costs[i] !== Infinity) n++;\n return n;\n}\n"],"names":["octile","dx","dy","ax","ay","goalOf","goal","r","x","y","findPath","g","from","to","options","cost","endOnly","maxNodes","start","isOpen","nearestOpen","W","n","best","cameFrom","closed","heap","MinHeap","s","visited","i","cx","cy","out","k","step","NAV_STEPS","canStep","nx","ny","ni","tentative","floodFill","maxCost","always","c","reachedCount","costs"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"navGrid.js","sources":["../../src/nav/navGrid.ts"],"sourcesContent":["import { isSolidTile, type CollisionGrid } from '../net/movement';\n\n/** A tile coordinate. */\nexport interface TilePoint {\n x: number;\n y: number;\n}\n\n/**\n * The ground a search walks: a width, a height and whether a body may stand on a tile. `walkable` is never asked\n * about a tile outside the grid, so a game's own test needs no bounds check.\n */\nexport interface NavGrid {\n readonly width: number;\n readonly height: number;\n walkable(tx: number, ty: number): boolean;\n}\n\n/** What a search may be told beyond the grid, shared by `findPath`, `floodFill` and `straightenPath`. */\nexport interface NavOptions {\n /**\n * Extra cost of stepping onto a tile, on top of the step itself (1 straight, √2 diagonal). Must not be negative,\n * or A* stops being exact. `nearWallCost` is one: it keeps a route off the edges of a clearing.\n */\n cost?: (tx: number, ty: number) => number;\n /**\n * Tiles a path may end on but never pass through or brush past diagonally: a door that throws a body onto another\n * map, a trap, a ledge. A start standing on one may still leave it.\n */\n endOnly?: (tx: number, ty: number) => boolean;\n}\n\n/** A grid over any walkability test. */\nexport function navGrid(width: number, height: number, walkable: (tx: number, ty: number) => boolean): NavGrid {\n return { width, height, walkable };\n}\n\n/** The engine's own collision grid as ground to search: a tile a body cannot stand on is a wall. */\nexport function navGridFromCollision(grid: CollisionGrid): NavGrid {\n return { width: grid.width, height: grid.height, walkable: (tx, ty) => !isSolidTile(grid, tx, ty) };\n}\n\n/** Inside the grid and walkable. */\nexport function isOpen(g: NavGrid, tx: number, ty: number): boolean {\n return tx >= 0 && ty >= 0 && tx < g.width && ty < g.height && g.walkable(tx, ty);\n}\n\n/**\n * A `cost` that charges `extra` for a tile with a wall beside it (four-way), so a route prefers the middle of a\n * clearing to the gap between two obstacles — the narrow way is where an auto-walking body wedges itself. 1.4 was\n * high enough to go round a thicket and low enough to still take a gate.\n */\nexport function nearWallCost(g: NavGrid, extra = 1.4): (tx: number, ty: number) => number {\n return (x, y) => (!isOpen(g, x + 1, y) || !isOpen(g, x - 1, y) || !isOpen(g, x, y + 1) || !isOpen(g, x, y - 1) ? extra : 0);\n}\n\n/** Eight-way steps; the diagonals cost √2 so a path does not zig-zag where a straight line would do. */\nexport const NAV_STEPS: readonly (readonly [number, number, number])[] = [\n [1, 0, 1],\n [-1, 0, 1],\n [0, 1, 1],\n [0, -1, 1],\n [1, 1, Math.SQRT2],\n [1, -1, Math.SQRT2],\n [-1, 1, Math.SQRT2],\n [-1, -1, Math.SQRT2],\n];\n\n/**\n * May a body step from (cx, cy) by (dx, dy)? The tile must be open and, when it is end-only, be the goal; a diagonal\n * may not cut the corner between two walls, nor brush an end-only tile on the way past.\n */\nexport function canStep(\n g: NavGrid,\n cx: number,\n cy: number,\n dx: number,\n dy: number,\n endOnly: ((tx: number, ty: number) => boolean) | undefined,\n isGoal: (tx: number, ty: number) => boolean,\n): boolean {\n const nx = cx + dx;\n const ny = cy + dy;\n if (!isOpen(g, nx, ny)) return false;\n if (endOnly && endOnly(nx, ny) && !isGoal(nx, ny)) return false;\n if (dx !== 0 && dy !== 0) {\n if (!isOpen(g, cx + dx, cy) || !isOpen(g, cx, cy + dy)) return false;\n if (endOnly && ((endOnly(cx + dx, cy) && !isGoal(cx + dx, cy)) || (endOnly(cx, cy + dy) && !isGoal(cx, cy + dy)))) return false;\n }\n return true;\n}\n"],"names":["navGrid","width","height","walkable","navGridFromCollision","grid","tx","ty","isSolidTile","isOpen","g","nearWallCost","extra","x","y","NAV_STEPS","canStep","cx","cy","dx","dy","endOnly","isGoal","nx","ny"],"mappings":";AAiCO,SAASA,EAAQC,GAAeC,GAAgBC,GAAwD;AAC7G,SAAO,EAAE,OAAAF,GAAO,QAAAC,GAAQ,UAAAC,EAAA;AAC1B;AAGO,SAASC,EAAqBC,GAA8B;AACjE,SAAO,EAAE,OAAOA,EAAK,OAAO,QAAQA,EAAK,QAAQ,UAAU,CAACC,GAAIC,MAAO,CAACC,EAAYH,GAAMC,GAAIC,CAAE,EAAA;AAClG;AAGO,SAASE,EAAOC,GAAYJ,GAAYC,GAAqB;AAClE,SAAOD,KAAM,KAAKC,KAAM,KAAKD,IAAKI,EAAE,SAASH,IAAKG,EAAE,UAAUA,EAAE,SAASJ,GAAIC,CAAE;AACjF;AAOO,SAASI,EAAaD,GAAYE,IAAQ,KAAyC;AACxF,SAAO,CAACC,GAAGC,MAAO,CAACL,EAAOC,GAAGG,IAAI,GAAGC,CAAC,KAAK,CAACL,EAAOC,GAAGG,IAAI,GAAGC,CAAC,KAAK,CAACL,EAAOC,GAAGG,GAAGC,IAAI,CAAC,KAAK,CAACL,EAAOC,GAAGG,GAAGC,IAAI,CAAC,IAAIF,IAAQ;AAC3H;AAGO,MAAMG,IAA4D;AAAA,EACvE,CAAC,GAAG,GAAG,CAAC;AAAA,EACR,CAAC,IAAI,GAAG,CAAC;AAAA,EACT,CAAC,GAAG,GAAG,CAAC;AAAA,EACR,CAAC,GAAG,IAAI,CAAC;AAAA,EACT,CAAC,GAAG,GAAG,KAAK,KAAK;AAAA,EACjB,CAAC,GAAG,IAAI,KAAK,KAAK;AAAA,EAClB,CAAC,IAAI,GAAG,KAAK,KAAK;AAAA,EAClB,CAAC,IAAI,IAAI,KAAK,KAAK;AACrB;AAMO,SAASC,EACdN,GACAO,GACAC,GACAC,GACAC,GACAC,GACAC,GACS;AACT,QAAMC,IAAKN,IAAKE,GACVK,IAAKN,IAAKE;AAGhB,SAFI,GAACX,EAAOC,GAAGa,GAAIC,CAAE,KACjBH,KAAWA,EAAQE,GAAIC,CAAE,KAAK,CAACF,EAAOC,GAAIC,CAAE,KAC5CL,MAAO,KAAKC,MAAO,MACjB,CAACX,EAAOC,GAAGO,IAAKE,GAAID,CAAE,KAAK,CAACT,EAAOC,GAAGO,GAAIC,IAAKE,CAAE,KACjDC,MAAaA,EAAQJ,IAAKE,GAAID,CAAE,KAAK,CAACI,EAAOL,IAAKE,GAAID,CAAE,KAAOG,EAAQJ,GAAIC,IAAKE,CAAE,KAAK,CAACE,EAAOL,GAAIC,IAAKE,CAAE;AAGlH;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"nearestOpen.js","sources":["../../src/nav/nearestOpen.ts"],"sourcesContent":["import { isOpen, type NavGrid, type TilePoint } from './navGrid';\n\n/**\n * The open tile nearest to (tx, ty) within `maxRadius` rings, or null: a start or a goal inside a rock still gets a\n * route. `ok` narrows what counts (an open tile that is not a door, say). On each ring the closest tile wins, not\n * the first scanned — scanning order once always picked the ring's top-left corner.\n */\nexport function nearestOpen(\n g: NavGrid,\n tx: number,\n ty: number,\n maxRadius = 12,\n ok: (x: number, y: number) => boolean = (x, y) => isOpen(g, x, y),\n): TilePoint | null {\n if (ok(tx, ty)) return { x: tx, y: ty };\n for (let r = 1; r <= maxRadius; r++) {\n let best: TilePoint | null = null;\n let bestD = Infinity;\n for (let dy = -r; dy <= r; dy++)\n for (let dx = -r; dx <= r; dx++) {\n if (Math.max(Math.abs(dx), Math.abs(dy)) !== r || !ok(tx + dx, ty + dy)) continue;\n const d = dx * dx + dy * dy;\n if (d < bestD) {\n bestD = d;\n best = { x: tx + dx, y: ty + dy };\n }\n }\n if (best) return best;\n }\n return null;\n}\n"],"names":["nearestOpen","g","tx","ty","maxRadius","ok","x","y","isOpen","r","best","bestD","dy","dx","d"],"mappings":";AAOO,SAASA,EACdC,GACAC,GACAC,GACAC,IAAY,IACZC,IAAwC,CAACC,GAAGC,MAAMC,EAAOP,GAAGK,GAAGC,CAAC,GAC9C;AAClB,MAAIF,EAAGH,GAAIC,CAAE,UAAU,EAAE,GAAGD,GAAI,GAAGC,EAAA;AACnC,WAASM,IAAI,GAAGA,KAAKL,GAAWK,KAAK;AACnC,QAAIC,IAAyB,MACzBC,IAAQ;AACZ,aAASC,IAAK,CAACH,GAAGG,KAAMH,GAAGG;AACzB,eAASC,IAAK,CAACJ,GAAGI,KAAMJ,GAAGI,KAAM;AAC/B,YAAI,KAAK,IAAI,KAAK,IAAIA,CAAE,GAAG,KAAK,IAAID,CAAE,CAAC,MAAMH,KAAK,CAACJ,EAAGH,IAAKW,GAAIV,IAAKS,CAAE,EAAG;AACzE,cAAME,IAAID,IAAKA,IAAKD,IAAKA;AACzB,QAAIE,IAAIH,MACNA,IAAQG,GACRJ,IAAO,EAAE,GAAGR,IAAKW,GAAI,GAAGV,IAAKS,EAAA;AAAA,MAEjC;AACF,QAAIF,EAAM,QAAOA;AAAA,EACnB;AACA,SAAO;AACT;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"straightenPath.js","sources":["../../src/nav/straightenPath.ts"],"sourcesContent":["import { canStep, type NavGrid, type NavOptions, type TilePoint } from './navGrid';\n\n/** How many tiles ahead `straightenPath` looks for a straighter way: enough for a long staircase, cheap per re-solve. */\nconst WINDOW = 48;\n\n/**\n * The same route with fewer turns. An eight-way search finds many equally short paths and hands back whichever its\n * heap settled on, often a staircase — diagonal, straight, diagonal, straight — and a body walking one turns on every\n * tile. Each stretch that can be is replaced with the plainest equal way through it: one diagonal run and one straight\n * run. A replacement must cost no more than the stretch did (by the same `cost`, so it never trades the middle of a\n * clearing for the edge of a wall), step only where the search could, and touch no end-only tile short of the end.\n * Pass the options the path was found with.\n */\nexport function straightenPath(g: NavGrid, path: readonly TilePoint[], options: NavOptions = {}): TilePoint[] {\n if (path.length < 3) return path.slice();\n const extra = options.cost;\n const stepCost = (x: number, y: number, diagonal: boolean) => (diagonal ? Math.SQRT2 : 1) + (extra ? extra(x, y) : 0);\n const upTo = [0];\n for (let k = 1; k < path.length; k++) {\n const a = path[k - 1]!;\n const b = path[k]!;\n upTo.push(upTo[k - 1]! + stepCost(b.x, b.y, a.x !== b.x && a.y !== b.y));\n }\n const last = path[path.length - 1]!;\n const out: TilePoint[] = [path[0]!];\n let i = 0;\n while (i < path.length - 1) {\n let taken = false;\n for (let j = Math.min(path.length - 1, i + WINDOW); j >= i + 2 && !taken; j--) {\n const budget = upTo[j]! - upTo[i]! + 1e-6;\n for (const diagonalFirst of [true, false]) {\n const legs = twoLegs(g, path[i]!, path[j]!, diagonalFirst, options.endOnly, last, budget, stepCost);\n if (!legs) continue;\n out.push(...legs);\n i = j;\n taken = true;\n break;\n }\n }\n if (!taken) out.push(path[++i]!);\n }\n return out;\n}\n\n/** The tiles after `a` up to `b` along one diagonal run and one straight run, or null when that way is shut or dearer. */\nfunction twoLegs(\n g: NavGrid,\n a: TilePoint,\n b: TilePoint,\n diagonalFirst: boolean,\n endOnly: ((tx: number, ty: number) => boolean) | undefined,\n last: TilePoint,\n budget: number,\n stepCost: (x: number, y: number, diagonal: boolean) => number,\n): TilePoint[] | null {\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const diag = Math.min(Math.abs(dx), Math.abs(dy));\n const straight = Math.max(Math.abs(dx), Math.abs(dy)) - diag;\n const d: [number, number] = [Math.sign(dx), Math.sign(dy)];\n const s: [number, number] = Math.abs(dx) > Math.abs(dy) ? [Math.sign(dx), 0] : [0, Math.sign(dy)];\n const runs: [[number, number], number][] = diagonalFirst ? [[d, diag], [s, straight]] : [[s, straight], [d, diag]];\n // only the path's own last tile may be end-only\n const isEnd = (x: number, y: number) => x === last.x && y === last.y;\n const tiles: TilePoint[] = [];\n let x = a.x;\n let y = a.y;\n let cost = 0;\n for (const [[sx, sy], n] of runs) {\n for (let k = 0; k < n; k++) {\n if (!canStep(g, x, y, sx, sy, endOnly, isEnd)) return null;\n x += sx;\n y += sy;\n cost += stepCost(x, y, sx !== 0 && sy !== 0);\n if (cost > budget) return null;\n tiles.push({ x, y });\n }\n }\n return tiles;\n}\n\n/** Drops the tiles a straight line already covers, so a trail is drawn as a few long legs rather than tile by tile. */\nexport function simplifyPath(path: readonly TilePoint[]): TilePoint[] {\n if (path.length < 3) return path.slice();\n const out: TilePoint[] = [path[0]!];\n for (let i = 1; i < path.length - 1; i++) {\n const a = out[out.length - 1]!;\n const b = path[i]!;\n const c = path[i + 1]!;\n if ((b.x - a.x) * (c.y - b.y) !== (b.y - a.y) * (c.x - b.x)) out.push(b);\n }\n out.push(path[path.length - 1]!);\n return out;\n}\n"],"names":["WINDOW","straightenPath","g","path","options","extra","stepCost","x","y","diagonal","upTo","k","a","b","last","out","i","taken","j","budget","diagonalFirst","legs","twoLegs","endOnly","dx","dy","diag","straight","d","s","runs","isEnd","tiles","cost","sx","sy","n","canStep","simplifyPath","c"],"mappings":";AAGA,MAAMA,IAAS;AAUR,SAASC,EAAeC,GAAYC,GAA4BC,IAAsB,CAAA,GAAiB;AAC5G,MAAID,EAAK,SAAS,EAAG,QAAOA,EAAK,MAAA;AACjC,QAAME,IAAQD,EAAQ,MAChBE,IAAW,CAACC,GAAWC,GAAWC,OAAuBA,IAAW,KAAK,QAAQ,MAAMJ,IAAQA,EAAME,GAAGC,CAAC,IAAI,IAC7GE,IAAO,CAAC,CAAC;AACf,WAASC,IAAI,GAAGA,IAAIR,EAAK,QAAQQ,KAAK;AACpC,UAAMC,IAAIT,EAAKQ,IAAI,CAAC,GACdE,IAAIV,EAAKQ,CAAC;AAChB,IAAAD,EAAK,KAAKA,EAAKC,IAAI,CAAC,IAAKL,EAASO,EAAE,GAAGA,EAAE,GAAGD,EAAE,MAAMC,EAAE,KAAKD,EAAE,MAAMC,EAAE,CAAC,CAAC;AAAA,EACzE;AACA,QAAMC,IAAOX,EAAKA,EAAK,SAAS,CAAC,GAC3BY,IAAmB,CAACZ,EAAK,CAAC,CAAE;AAClC,MAAIa,IAAI;AACR,SAAOA,IAAIb,EAAK,SAAS,KAAG;AAC1B,QAAIc,IAAQ;AACZ,aAASC,IAAI,KAAK,IAAIf,EAAK,SAAS,GAAGa,IAAIhB,CAAM,GAAGkB,KAAKF,IAAI,KAAK,CAACC,GAAOC,KAAK;AAC7E,YAAMC,IAAST,EAAKQ,CAAC,IAAKR,EAAKM,CAAC,IAAK;AACrC,iBAAWI,KAAiB,CAAC,IAAM,EAAK,GAAG;AACzC,cAAMC,IAAOC,EAAQpB,GAAGC,EAAKa,CAAC,GAAIb,EAAKe,CAAC,GAAIE,GAAehB,EAAQ,SAASU,GAAMK,GAAQb,CAAQ;AAClG,YAAKe,GACL;AAAA,UAAAN,EAAI,KAAK,GAAGM,CAAI,GAChBL,IAAIE,GACJD,IAAQ;AACR;AAAA;AAAA,MACF;AAAA,IACF;AACA,IAAKA,KAAOF,EAAI,KAAKZ,EAAK,EAAEa,CAAC,CAAE;AAAA,EACjC;AACA,SAAOD;AACT;AAGA,SAASO,EACPpB,GACAU,GACAC,GACAO,GACAG,GACAT,GACAK,GACAb,GACoB;AACpB,QAAMkB,IAAKX,EAAE,IAAID,EAAE,GACba,IAAKZ,EAAE,IAAID,EAAE,GACbc,IAAO,KAAK,IAAI,KAAK,IAAIF,CAAE,GAAG,KAAK,IAAIC,CAAE,CAAC,GAC1CE,IAAW,KAAK,IAAI,KAAK,IAAIH,CAAE,GAAG,KAAK,IAAIC,CAAE,CAAC,IAAIC,GAClDE,IAAsB,CAAC,KAAK,KAAKJ,CAAE,GAAG,KAAK,KAAKC,CAAE,CAAC,GACnDI,IAAsB,KAAK,IAAIL,CAAE,IAAI,KAAK,IAAIC,CAAE,IAAI,CAAC,KAAK,KAAKD,CAAE,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,KAAKC,CAAE,CAAC,GAC1FK,IAAqCV,IAAgB,CAAC,CAACQ,GAAGF,CAAI,GAAG,CAACG,GAAGF,CAAQ,CAAC,IAAI,CAAC,CAACE,GAAGF,CAAQ,GAAG,CAACC,GAAGF,CAAI,CAAC,GAE3GK,IAAQ,CAACxB,GAAWC,MAAcD,MAAMO,EAAK,KAAKN,MAAMM,EAAK,GAC7DkB,IAAqB,CAAA;AAC3B,MAAIzB,IAAIK,EAAE,GACNJ,IAAII,EAAE,GACNqB,IAAO;AACX,aAAW,CAAC,CAACC,GAAIC,CAAE,GAAGC,CAAC,KAAKN;AAC1B,aAAS,IAAI,GAAG,IAAIM,GAAG,KAAK;AAK1B,UAJI,CAACC,EAAQnC,GAAGK,GAAGC,GAAG0B,GAAIC,GAAIZ,GAASQ,CAAK,MAC5CxB,KAAK2B,GACL1B,KAAK2B,GACLF,KAAQ3B,EAASC,GAAGC,GAAG0B,MAAO,KAAKC,MAAO,CAAC,GACvCF,IAAOd,GAAQ,QAAO;AAC1B,MAAAa,EAAM,KAAK,EAAE,GAAAzB,GAAG,GAAAC,EAAA,CAAG;AAAA,IACrB;AAEF,SAAOwB;AACT;AAGO,SAASM,EAAanC,GAAyC;AACpE,MAAIA,EAAK,SAAS,EAAG,QAAOA,EAAK,MAAA;AACjC,QAAMY,IAAmB,CAACZ,EAAK,CAAC,CAAE;AAClC,WAASa,IAAI,GAAGA,IAAIb,EAAK,SAAS,GAAGa,KAAK;AACxC,UAAMJ,IAAIG,EAAIA,EAAI,SAAS,CAAC,GACtBF,IAAIV,EAAKa,CAAC,GACVuB,IAAIpC,EAAKa,IAAI,CAAC;AACpB,KAAKH,EAAE,IAAID,EAAE,MAAM2B,EAAE,IAAI1B,EAAE,QAAQA,EAAE,IAAID,EAAE,MAAM2B,EAAE,IAAI1B,EAAE,MAAIE,EAAI,KAAKF,CAAC;AAAA,EACzE;AACA,SAAAE,EAAI,KAAKZ,EAAKA,EAAK,SAAS,CAAC,CAAE,GACxBY;AACT;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"Connection.js","sources":["../../src/net/Connection.ts"],"sourcesContent":["import { EventBus } from '../events/EventBus';\nimport type { InputStep } from './constants';\nimport { noteServerTime } from './serverClock';\nimport { decodeSnapshotFrame } from './snapshotCodec';\nimport type { ClientMessage, ServerMessage, Snapshot } from './protocol';\n\nexport interface ConnectionEvents {\n open: undefined;\n /** The socket closed; `willRetry` when it will reconnect by itself. */\n close: { code: number; reason: string; willRetry: boolean };\n welcome: Extract<ServerMessage, { t: 'welcome' }>;\n snapshot: Snapshot;\n /** Any message the engine does not handle: the game's own. */\n message: { t: string; [k: string]: unknown };\n}\n\nexport interface ConnectionOptions {\n /** Reconnect after the socket drops (default true), waiting longer each time up to `maxBackoffMs`. */\n reconnect?: boolean;\n maxBackoffMs?: number;\n /** Ping interval for the server clock and the round-trip time (default 1000 ms). */\n pingMs?: number;\n /** Sent on every (re)connect as `hello.data`. */\n hello?: () => unknown;\n /** The WebSocket class (default the global one; Node 22+ and browsers have it). */\n WebSocket?: typeof WebSocket;\n}\n\n/**\n * The client side of the engine's netcode over a WebSocket: hello on open, input steps batched and sent once a frame\n * (`queueInput` then `flush`), snapshots decoded (binary or JSON) onto `events`, pings that keep the server clock\n * (`serverNow`) and `rtt` up to date, and reconnection with backoff.\n */\nexport class Connection {\n readonly events = new EventBus<ConnectionEvents>();\n /** The last measured round trip, ms. */\n rtt = 0;\n private socket: WebSocket | null = null;\n private batch: { seq: number; steps: InputStep[] } | null = null;\n private closedByUs = false;\n private attempt = 0;\n private pingTimer: ReturnType<typeof setInterval> | null = null;\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(\n readonly url: string,\n private readonly options: ConnectionOptions = {},\n ) {}\n\n get open(): boolean {\n return this.socket?.readyState === 1;\n }\n\n connect(): this {\n this.closedByUs = false;\n const Impl = this.options.WebSocket ?? globalThis.WebSocket;\n if (!Impl) throw new Error('Connection: no WebSocket in this environment');\n const ws = new Impl(this.url);\n ws.binaryType = 'arraybuffer';\n this.socket = ws;\n ws.onopen = () => {\n this.attempt = 0;\n this.send({ t: 'hello', data: this.options.hello?.() });\n this.ping();\n this.pingTimer = setInterval(() => this.ping(), this.options.pingMs ?? 1000);\n this.events.emit('open', undefined);\n };\n ws.onmessage = (e) => this.receive(e.data);\n ws.onclose = (e) => {\n if (this.pingTimer) clearInterval(this.pingTimer);\n this.pingTimer = null;\n this.socket = null;\n const willRetry = !this.closedByUs && this.options.reconnect !== false;\n this.events.emit('close', { code: e.code, reason: e.reason, willRetry });\n if (willRetry) {\n const wait = Math.min(this.options.maxBackoffMs ?? 10_000, 250 * 2 ** this.attempt++);\n this.retryTimer = setTimeout(() => this.connect(), wait);\n }\n };\n return this;\n }\n\n /** Sends a message now (dropped while closed: input is resent by the prediction's own state, not by the socket). */\n send(msg: ClientMessage | { t: string; [k: string]: unknown }): void {\n if (this.socket?.readyState === 1) this.socket.send(JSON.stringify(msg));\n }\n\n /** Adds one input step to this frame's batch; `flush` sends it. Steps must come in sequence order. */\n queueInput(seq: number, step: InputStep): void {\n if (!this.batch) this.batch = { seq, steps: [] };\n this.batch.steps.push([step[0], step[1], step[2]]);\n }\n\n /** Sends this frame's input steps as one message. Call once a frame. */\n flush(): void {\n if (!this.batch) return;\n this.send({ t: 'input', seq: this.batch.seq, steps: this.batch.steps });\n this.batch = null;\n }\n\n close(): void {\n this.closedByUs = true;\n if (this.retryTimer) clearTimeout(this.retryTimer);\n if (this.pingTimer) clearInterval(this.pingTimer);\n this.socket?.close();\n this.socket = null;\n }\n\n private ping(): void {\n this.send({ t: 'ping', c: Date.now() });\n }\n\n private receive(data: unknown): void {\n let msg: ServerMessage | { t: string; [k: string]: unknown };\n try {\n msg = typeof data === 'string' ? JSON.parse(data) : (decodeSnapshotFrame<Snapshot>(data as ArrayBuffer) as Snapshot);\n } catch (err) {\n console.error('[net] could not read a message', err);\n return;\n }\n switch (msg.t) {\n case 'pong': {\n const m = msg as Extract<ServerMessage, { t: 'pong' }>;\n this.rtt = Date.now() - m.c;\n noteServerTime(m.st, this.rtt);\n return;\n }\n case 'welcome':\n this.events.emit('welcome', msg as Extract<ServerMessage, { t: 'welcome' }>);\n return;\n case 'snap':\n this.events.emit('snapshot', msg as Snapshot);\n return;\n default:\n this.events.emit('message', msg as { t: string; [k: string]: unknown });\n }\n }\n}\n"],"names":["Connection","url","options","EventBus","Impl","ws","e","willRetry","wait","msg","seq","step","data","decodeSnapshotFrame","err","m","noteServerTime"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"InterpolationBuffer.js","sources":["../../src/net/InterpolationBuffer.ts"],"sourcesContent":["import type { EntityUpdate } from './constants';\n\n/** One body's state at one server time. */\ninterface Sample {\n time: number;\n x: number;\n y: number;\n aim: number;\n anim: number;\n hp: number;\n flags: number;\n}\n\nexport interface InterpolatedState {\n x: number;\n y: number;\n aim: number;\n /** Discrete fields come from the older of the two samples until the newer one is reached. */\n anim: number;\n hp: number;\n flags: number;\n}\n\n/**\n * Smooth motion for bodies the server moves (other players, monsters): snapshots arrive 10–30 times a second and\n * jittered, so each body is drawn a little in the past (`delayMs`) between the two snapshots around that moment.\n * Past the newest snapshot it extrapolates for at most `maxExtrapolateMs`, then holds.\n *\n * Times are server times (`Snapshot.time`); render at `serverNow()`.\n */\nexport class InterpolationBuffer {\n private readonly samples = new Map<number, Sample[]>();\n\n constructor(\n /** How far in the past bodies are drawn (default 100 ms: two snapshots at 20 Hz, so one may be late). */\n readonly delayMs = 100,\n private readonly maxExtrapolateMs = 120,\n private readonly keep = 30,\n ) {}\n\n /** Records every body of a snapshot taken at `time`. */\n push(time: number, updates: readonly EntityUpdate[]): void {\n for (const [id, x, y, aim, anim, hp, flags] of updates) {\n let list = this.samples.get(id);\n if (!list) {\n list = [];\n this.samples.set(id, list);\n }\n const last = list[list.length - 1];\n // out of order or repeated: a newer sample is already here\n if (last && time <= last.time) continue;\n list.push({ time, x, y, aim, anim, hp, flags });\n if (list.length > this.keep) list.splice(0, list.length - this.keep);\n }\n }\n\n /** Forgets a body (it left view). */\n remove(id: number): void {\n this.samples.delete(id);\n }\n\n has(id: number): boolean {\n return this.samples.has(id);\n }\n\n ids(): IterableIterator<number> {\n return this.samples.keys();\n }\n\n /** Where body `id` should be drawn at server time `now`, or null if nothing is known of it. */\n sample(id: number, now: number): InterpolatedState | null {\n const list = this.samples.get(id);\n if (!list || list.length === 0) return null;\n const t = now - this.delayMs;\n const first = list[0]!;\n if (t <= first.time || list.length === 1) return pick(first);\n for (let i = list.length - 1; i > 0; i--) {\n const a = list[i - 1]!;\n const b = list[i]!;\n if (t >= a.time && t <= b.time) {\n const k = b.time === a.time ? 1 : (t - a.time) / (b.time - a.time);\n return blend(a, b, k);\n }\n }\n // newer than every sample: carry on along the last motion, for a short while only\n const a = list[list.length - 2]!;\n const b = list[list.length - 1]!;\n const over = Math.min(t - b.time, this.maxExtrapolateMs);\n const k = b.time === a.time ? 1 : 1 + over / (b.time - a.time);\n return blend(a, b, k);\n }\n}\n\nfunction pick(s: Sample): InterpolatedState {\n return { x: s.x, y: s.y, aim: s.aim, anim: s.anim, hp: s.hp, flags: s.flags };\n}\n\nfunction blend(a: Sample, b: Sample, k: number): InterpolatedState {\n let d = b.aim - a.aim;\n if (d > Math.PI) d -= Math.PI * 2;\n if (d < -Math.PI) d += Math.PI * 2;\n const older = k < 1 ? a : b;\n return { x: a.x + (b.x - a.x) * k, y: a.y + (b.y - a.y) * k, aim: a.aim + d * k, anim: older.anim, hp: older.hp, flags: older.flags };\n}\n"],"names":["InterpolationBuffer","delayMs","maxExtrapolateMs","keep","time","updates","id","x","y","aim","anim","hp","flags","list","last","now","t","first","pick","a","b","k","blend","over","s","d","older"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"PredictedMover.js","sources":["../../src/net/PredictedMover.ts"],"sourcesContent":["import type { InputStep, MovementParams } from './constants';\nimport { createMover, stepMover, type CollisionGrid, type MoverState } from './movement';\nimport { moverFromWire, type MoverWire } from './protocol';\n\n/**\n * Client-side prediction for the local player's body. Every input step is applied at once (the player sees their\n * own move with no lag) and kept until the server acknowledges it. When a snapshot says where the server has the\n * body as of step `ack`, the body is put there and every step after `ack` is replayed on top.\n *\n * Movement is deterministic (`stepMover`), so with the same steps, params and grid the replay lands exactly where\n * the prediction was and nothing moves. When it does not (the server refused a step, a collision only it knew of),\n * the difference is `correction`: draw the body at `drawX/drawY`, which ease that jump out over a few frames instead\n * of snapping.\n */\nexport class PredictedMover {\n readonly mover: MoverState;\n private pending: { seq: number; step: InputStep }[] = [];\n private nextSeq = 0;\n /** The jump the last reconciliation made, still being eased out (world px). */\n private offsetX = 0;\n private offsetY = 0;\n /** How big the last correction was (0 when the prediction was right). */\n correction = 0;\n /** Share of a visual correction left after each 60th of a second (default 0.8). */\n smoothing = 0.8;\n\n constructor(\n x: number,\n y: number,\n private params: MovementParams,\n private grid: CollisionGrid,\n ) {\n this.mover = createMover(x, y);\n }\n\n /** Applies one input step now and keeps it for replay; returns its sequence number (send it with the step). */\n step(step: InputStep): number {\n const seq = this.nextSeq++;\n this.pending.push({ seq, step: [step[0], step[1], step[2]] });\n stepMover(this.mover, step, this.params, this.grid);\n return seq;\n }\n\n /** Steps sent but not yet acknowledged. */\n get unacknowledged(): number {\n return this.pending.length;\n }\n\n /** The server's word: the body as of step `ack`. Rewinds there and replays the rest. */\n reconcile(ack: number, server: MoverWire): void {\n const beforeX = this.mover.x;\n const beforeY = this.mover.y;\n let drop = 0;\n while (drop < this.pending.length && this.pending[drop]!.seq <= ack) drop++;\n if (drop) this.pending = this.pending.slice(drop);\n moverFromWire(server, this.mover);\n for (const p of this.pending) stepMover(this.mover, p.step, this.params, this.grid);\n const dx = beforeX - this.mover.x;\n const dy = beforeY - this.mover.y;\n this.correction = Math.hypot(dx, dy);\n // a correction this small is float noise; one this large is a teleport, shown at once\n if (this.correction > 0.01 && this.correction < 64) {\n this.offsetX += dx;\n this.offsetY += dy;\n } else if (this.correction >= 64) {\n this.offsetX = this.offsetY = 0;\n }\n }\n\n /** Eases the visual correction out; call once a frame with its length in seconds. */\n smooth(dt: number): void {\n const k = Math.pow(this.smoothing, dt * 60);\n this.offsetX *= k;\n this.offsetY *= k;\n if (Math.abs(this.offsetX) < 0.01) this.offsetX = 0;\n if (Math.abs(this.offsetY) < 0.01) this.offsetY = 0;\n }\n\n /** Where to draw the body: the predicted place plus the correction still being eased out. */\n get drawX(): number {\n return this.mover.x + this.offsetX;\n }\n\n get drawY(): number {\n return this.mover.y + this.offsetY;\n }\n\n /** New movement rules (a buff, a slow): applied to future steps and to replays. */\n setParams(params: MovementParams): void {\n this.params = params;\n }\n\n setGrid(grid: CollisionGrid): void {\n this.grid = grid;\n }\n}\n"],"names":["PredictedMover","x","y","params","grid","createMover","step","seq","stepMover","ack","server","beforeX","beforeY","drop","moverFromWire","p","dx","dy","dt","k"],"mappings":";;AAcO,MAAMA,EAAe;AAAA,EAY1B,YACEC,GACAC,GACQC,GACAC,GACR;AAFQ,SAAA,SAAAD,GACA,KAAA,OAAAC,GAER,KAAK,QAAQC,EAAYJ,GAAGC,CAAC;AAAA,EAC/B;AAAA,EAJU;AAAA,EACA;AAAA,EAfD;AAAA,EACD,UAA8C,CAAA;AAAA,EAC9C,UAAU;AAAA;AAAA,EAEV,UAAU;AAAA,EACV,UAAU;AAAA;AAAA,EAElB,aAAa;AAAA;AAAA,EAEb,YAAY;AAAA;AAAA,EAYZ,KAAKI,GAAyB;AAC5B,UAAMC,IAAM,KAAK;AACjB,gBAAK,QAAQ,KAAK,EAAE,KAAAA,GAAK,MAAM,CAACD,EAAK,CAAC,GAAGA,EAAK,CAAC,GAAGA,EAAK,CAAC,CAAC,GAAG,GAC5DE,EAAU,KAAK,OAAOF,GAAM,KAAK,QAAQ,KAAK,IAAI,GAC3CC;AAAA,EACT;AAAA;AAAA,EAGA,IAAI,iBAAyB;AAC3B,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA,EAGA,UAAUE,GAAaC,GAAyB;AAC9C,UAAMC,IAAU,KAAK,MAAM,GACrBC,IAAU,KAAK,MAAM;AAC3B,QAAIC,IAAO;AACX,WAAOA,IAAO,KAAK,QAAQ,UAAU,KAAK,QAAQA,CAAI,EAAG,OAAOJ,IAAK,CAAAI;AACrE,IAAIA,MAAM,KAAK,UAAU,KAAK,QAAQ,MAAMA,CAAI,IAChDC,EAAcJ,GAAQ,KAAK,KAAK;AAChC,eAAWK,KAAK,KAAK,QAAS,CAAAP,EAAU,KAAK,OAAOO,EAAE,MAAM,KAAK,QAAQ,KAAK,IAAI;AAClF,UAAMC,IAAKL,IAAU,KAAK,MAAM,GAC1BM,IAAKL,IAAU,KAAK,MAAM;AAChC,SAAK,aAAa,KAAK,MAAMI,GAAIC,CAAE,GAE/B,KAAK,aAAa,QAAQ,KAAK,aAAa,MAC9C,KAAK,WAAWD,GAChB,KAAK,WAAWC,KACP,KAAK,cAAc,OAC5B,KAAK,UAAU,KAAK,UAAU;AAAA,EAElC;AAAA;AAAA,EAGA,OAAOC,GAAkB;AACvB,UAAMC,IAAI,KAAK,IAAI,KAAK,WAAWD,IAAK,EAAE;AAC1C,SAAK,WAAWC,GAChB,KAAK,WAAWA,GACZ,KAAK,IAAI,KAAK,OAAO,IAAI,cAAW,UAAU,IAC9C,KAAK,IAAI,KAAK,OAAO,IAAI,cAAW,UAAU;AAAA,EACpD;AAAA;AAAA,EAGA,IAAI,QAAgB;AAClB,WAAO,KAAK,MAAM,IAAI,KAAK;AAAA,EAC7B;AAAA,EAEA,IAAI,QAAgB;AAClB,WAAO,KAAK,MAAM,IAAI,KAAK;AAAA,EAC7B;AAAA;AAAA,EAGA,UAAUhB,GAA8B;AACtC,SAAK,SAASA;AAAA,EAChB;AAAA,EAEA,QAAQC,GAA2B;AACjC,SAAK,OAAOA;AAAA,EACd;AACF;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"Room.js","sources":["../../src/net/Room.ts"],"sourcesContent":["import type { EntityUpdate, InputStep, MovementParams } from './constants';\nimport { createMover, stepMover, type CollisionGrid, type MoverState } from './movement';\nimport { moverToWire, type Snapshot } from './protocol';\n\nexport interface RoomPlayer<D = unknown> {\n readonly id: number;\n readonly mover: MoverState;\n /** The game's own data for this player (sent to others in `sp` when they first see it). */\n data: D;\n params: MovementParams;\n /** The last input step applied (-1 before any): what snapshots acknowledge. */\n ack: number;\n /** Steps received and not yet applied. */\n readonly queue: InputStep[];\n /** What the snapshot's update record carries beside the position; the game sets them. */\n aim: number;\n anim: number;\n hp: number;\n flags: number;\n /** Refused inputs (nonsense numbers, a flood): a game may kick past some count. */\n strikes: number;\n}\n\nexport interface RoomOptions<D> {\n grid: CollisionGrid;\n /** Movement rules a new player starts with. */\n params: MovementParams;\n /** Where a player who joins stands. */\n spawn: (id: number, data: D) => { x: number; y: number };\n /** Steps a player may have waiting before more are refused (default 30: half a second at 60). */\n maxQueue?: number;\n /** A player with more than `backlog` waiting catches up by running up to `maxStepsPerTick` a tick (default 3, 4). */\n maxStepsPerTick?: number;\n backlog?: number;\n /** Called after each step a player takes (set aim, anim, flags from it). */\n onStep?: (player: RoomPlayer<D>, step: InputStep) => void;\n}\n\n/**\n * An authoritative room for a server: players send input steps, the room applies them with the same `stepMover` the\n * clients predict with, and builds each player a snapshot of everyone (with their own body whole, so their\n * prediction can rewind to it). Transport-free: wire `input`, `step` and `snapshotFor` to any socket library.\n *\n * Every step a player sends is applied once, in order; a client cannot move faster by sending more (the queue is\n * capped and drained at most `maxStepsPerTick` a tick), and nonsense input is refused and counted.\n */\nexport class Room<D = unknown> {\n readonly players = new Map<number, RoomPlayer<D>>();\n tick = 0;\n private readonly known = new Map<number, Set<number>>();\n private readonly maxQueue: number;\n private readonly maxSteps: number;\n private readonly backlog: number;\n\n constructor(private readonly options: RoomOptions<D>) {\n this.maxQueue = options.maxQueue ?? 30;\n this.maxSteps = options.maxStepsPerTick ?? 3;\n this.backlog = options.backlog ?? 4;\n }\n\n join(id: number, data: D): RoomPlayer<D> {\n if (this.players.has(id)) throw new Error(`Room: player ${id} has already joined`);\n const at = this.options.spawn(id, data);\n const player: RoomPlayer<D> = {\n id,\n mover: createMover(at.x, at.y),\n data,\n params: { ...this.options.params },\n ack: -1,\n queue: [],\n aim: 0,\n anim: 0,\n hp: 0,\n flags: 0,\n strikes: 0,\n };\n this.players.set(id, player);\n this.known.set(id, new Set());\n return player;\n }\n\n leave(id: number): void {\n this.players.delete(id);\n this.known.delete(id);\n }\n\n /**\n * Input steps `seq`, `seq + 1`, … from a player. Steps already received are skipped (a resend), any step with\n * numbers no client sends refuses the whole message, and steps past a full queue are dropped. Returns false when\n * something was refused.\n */\n input(id: number, seq: number, steps: readonly InputStep[]): boolean {\n const p = this.players.get(id);\n if (!p) return false;\n if (!Number.isInteger(seq) || seq < 0 || !Array.isArray(steps) || steps.length > this.maxQueue) {\n p.strikes++;\n return false;\n }\n for (const s of steps) {\n if (!Array.isArray(s) || s.length !== 3 || !s.every((v) => Number.isFinite(v)) || Math.abs(s[0]) > 1 || Math.abs(s[1]) > 1 || !Number.isInteger(s[2])) {\n p.strikes++;\n return false;\n }\n }\n const next = p.ack + 1 + p.queue.length;\n // a gap (steps this room refused earlier) is filled with standing still, so sequence numbers stay the client's\n const gap = seq - next;\n if (gap > this.maxQueue) {\n p.strikes++;\n return false;\n }\n for (let i = 0; i < gap; i++) p.queue.push([0, 0, 0]);\n let ok = true;\n for (let i = 0; i < steps.length; i++) {\n if (seq + i < next) continue; // already have it: a resend\n if (p.queue.length >= this.maxQueue) {\n ok = false;\n p.strikes++;\n break;\n }\n p.queue.push([steps[i]![0], steps[i]![1], steps[i]![2]]);\n }\n return ok;\n }\n\n /** One simulation step for everyone. */\n step(): void {\n this.tick++;\n for (const p of this.players.values()) {\n const waiting = p.queue.length;\n if (waiting === 0) continue;\n const n = waiting > this.backlog ? Math.min(this.maxSteps, waiting - this.backlog + 1) : 1;\n for (let i = 0; i < n; i++) {\n const s = p.queue.shift()!;\n stepMover(p.mover, s, p.params, this.options.grid);\n p.ack++;\n this.options.onStep?.(p, s);\n }\n }\n }\n\n /** The snapshot player `id` should get now. `time` is the server's wall clock (ms). */\n snapshotFor(id: number, time: number): Snapshot | null {\n const me = this.players.get(id);\n const known = this.known.get(id);\n if (!me || !known) return null;\n const up: EntityUpdate[] = [];\n const sp: { id: number; data?: unknown }[] = [];\n const seen = new Set<number>();\n for (const p of this.players.values()) {\n seen.add(p.id);\n up.push([p.id, p.mover.x, p.mover.y, p.aim, p.anim, p.hp, p.flags]);\n if (!known.has(p.id)) {\n known.add(p.id);\n sp.push({ id: p.id, data: p.data });\n }\n }\n const rm: number[] = [];\n for (const k of known) {\n if (!seen.has(k)) {\n known.delete(k);\n rm.push(k);\n }\n }\n const snap: Snapshot = { t: 'snap', tick: this.tick, time, ack: me.ack, me: moverToWire(me.mover), up };\n if (sp.length) snap.sp = sp;\n if (rm.length) snap.rm = rm;\n return snap;\n }\n}\n"],"names":["Room","options","id","data","at","player","createMover","seq","steps","p","v","next","gap","i","ok","waiting","n","s","stepMover","time","me","known","up","sp","seen","rm","k","snap","moverToWire"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"RoomHost.js","sources":["../../src/net/RoomHost.ts"],"sourcesContent":["import type { Room } from './Room';\nimport { encodeSnapshotFrame } from './snapshotCodec';\nimport type { ClientMessage, ServerMessage, Snapshot } from './protocol';\n\n/** The least a server socket must do; a `ws` WebSocket, a uWebSockets one or a test double all fit. */\nexport interface HostSocket {\n send(data: string | Uint8Array): void;\n close(code?: number, reason?: string): void;\n}\n\nexport interface RoomHostOptions<D> {\n /** Simulation steps a second (default 60) and snapshots a second (default 20). */\n stepRate?: number;\n snapRate?: number;\n /** Turns a hello's data into the player's data (validate it here); throw to refuse the player. */\n admit?: (data: unknown, id: number) => D;\n /** Refused inputs before the player is disconnected (default 20). */\n maxStrikes?: number;\n /** The game's own messages (anything the engine does not handle). */\n onMessage?: (id: number, msg: { t: string; [k: string]: unknown }) => void;\n /** Called when a player joins or leaves. */\n onJoin?: (id: number) => void;\n onLeave?: (id: number) => void;\n /** Wall clock, for tests (default Date.now). */\n now?: () => number;\n}\n\n/**\n * Runs a `Room` for connected sockets: a socket says hello and joins, its input goes to the room, the room steps at\n * `stepRate`, and every player gets its own snapshot at `snapRate` (binary when it carries bodies). Pings are\n * answered with the server's time. A player who keeps sending nonsense is disconnected.\n *\n * ```ts\n * const host = new RoomHost(room);\n * wss.on('connection', (ws) => {\n * const id = host.connect(ws);\n * ws.on('message', (data, binary) => host.message(id, binary ? data : data.toString()));\n * ws.on('close', () => host.disconnect(id));\n * });\n * host.start();\n * ```\n */\nexport class RoomHost<D = unknown> {\n private readonly sockets = new Map<number, HostSocket>();\n private readonly joined = new Set<number>();\n private nextId = 1;\n private stepTimer: ReturnType<typeof setInterval> | null = null;\n private lastStepAt = 0;\n private stepsDue = 0;\n private readonly stepMs: number;\n private readonly snapEvery: number;\n private readonly now: () => number;\n\n constructor(\n readonly room: Room<D>,\n private readonly options: RoomHostOptions<D> = {},\n ) {\n this.stepMs = 1000 / (options.stepRate ?? 60);\n this.snapEvery = Math.max(1, Math.round((options.stepRate ?? 60) / (options.snapRate ?? 20)));\n this.now = options.now ?? Date.now;\n }\n\n /** A socket connected; returns the id its messages and its close are reported under. */\n connect(socket: HostSocket): number {\n const id = this.nextId++;\n this.sockets.set(id, socket);\n return id;\n }\n\n /** A message arrived from socket `id` (text as a string, binary as bytes). */\n message(id: number, data: string | ArrayBuffer | Uint8Array): void {\n if (typeof data !== 'string') return; // clients send JSON only\n let msg: ClientMessage | { t: string; [k: string]: unknown };\n try {\n msg = JSON.parse(data);\n } catch {\n return this.strike(id);\n }\n if (!msg || typeof msg !== 'object' || typeof msg.t !== 'string') return this.strike(id);\n switch (msg.t) {\n case 'hello': {\n if (this.joined.has(id)) return;\n let data: D;\n try {\n data = this.options.admit ? this.options.admit((msg as { data?: unknown }).data, id) : ((msg as { data?: unknown }).data as D);\n } catch (err) {\n this.sockets.get(id)?.close(4003, err instanceof Error ? err.message : 'refused');\n return;\n }\n this.room.join(id, data);\n this.joined.add(id);\n this.sendTo(id, { t: 'welcome', id, tick: this.room.tick, stepRate: 1000 / this.stepMs, snapRate: (1000 / this.stepMs) / this.snapEvery });\n this.options.onJoin?.(id);\n return;\n }\n case 'input': {\n const m = msg as Extract<ClientMessage, { t: 'input' }>;\n if (!this.joined.has(id)) return;\n if (!this.room.input(id, m.seq, m.steps)) this.checkStrikes(id);\n return;\n }\n case 'ping': {\n const c = (msg as { c?: unknown }).c;\n if (typeof c === 'number' && Number.isFinite(c)) this.sendTo(id, { t: 'pong', c, st: this.now() });\n return;\n }\n default:\n if (this.joined.has(id)) this.options.onMessage?.(id, msg as { t: string; [k: string]: unknown });\n }\n }\n\n /** Socket `id` closed. */\n disconnect(id: number): void {\n this.sockets.delete(id);\n if (this.joined.delete(id)) {\n this.room.leave(id);\n this.options.onLeave?.(id);\n }\n }\n\n /** Sends one of the game's own messages to a player. */\n sendTo(id: number, msg: ServerMessage | { t: string; [k: string]: unknown }): void {\n const s = this.sockets.get(id);\n if (!s) return;\n if ((msg as Snapshot).t === 'snap' && (msg as Snapshot).up?.length) s.send(encodeSnapshotFrame(msg as Snapshot));\n else s.send(JSON.stringify(msg));\n }\n\n /** Starts stepping on a timer. Steps are counted by the clock, so a late timer runs the steps it owes. */\n start(): void {\n if (this.stepTimer) return;\n this.lastStepAt = this.now();\n this.stepTimer = setInterval(() => {\n const now = this.now();\n this.stepsDue += (now - this.lastStepAt) / this.stepMs;\n this.lastStepAt = now;\n // a stall longer than a second is not caught up: the room just carries on\n this.stepsDue = Math.min(this.stepsDue, 60);\n while (this.stepsDue >= 1) {\n this.stepsDue--;\n this.stepOnce();\n }\n }, this.stepMs / 2);\n }\n\n stop(): void {\n if (this.stepTimer) clearInterval(this.stepTimer);\n this.stepTimer = null;\n }\n\n /** One step, and the snapshots when they are due. Exposed for tests and for servers that drive their own loop. */\n stepOnce(): void {\n this.room.step();\n if (this.room.tick % this.snapEvery !== 0) return;\n const now = this.now();\n for (const id of this.joined) {\n const snap = this.room.snapshotFor(id, now);\n if (snap) this.sendTo(id, snap);\n }\n }\n\n private strike(id: number): void {\n const p = this.room.players.get(id);\n if (p) {\n p.strikes++;\n this.checkStrikes(id);\n }\n }\n\n private checkStrikes(id: number): void {\n const p = this.room.players.get(id);\n if (p && p.strikes >= (this.options.maxStrikes ?? 20)) {\n this.sockets.get(id)?.close(4008, 'too much invalid input');\n this.disconnect(id);\n }\n }\n}\n"],"names":["RoomHost","room","options","socket","id","data","msg","err","m","c","s","encodeSnapshotFrame","now","snap","p"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"cadence.js","sources":["../../src/net/cadence.ts"],"sourcesContent":["/**\n * Every repeated player action with a timer — the basic attack, a skill's cooldown, the cast and attack locks, a\n * potion — runs on a **schedule**, not on \"the last time plus some slack\". An action may arrive up to its slack\n * early (network jitter bunches packets), but the next one is then due a whole interval after the later of the\n * arrival and the time this one was due, so the slack never adds up: a client sending every 0.8 of an interval\n * lands at most one per interval (67 in 100), not 100. A server must run the same arithmetic.\n */\nexport const CADENCE = {\n /** A basic attack may arrive this share of its interval early. */\n attackEarlyShare: 0.2,\n /** The cast lock and the attack lock a cast leaves may be cut this short. */\n lockSlackMs: 60,\n /** A skill may be cast this long before its cooldown ends. */\n cooldownSlackMs: 40,\n /** A potion may be drunk this long before its kind's cooldown ends. */\n potionSlackMs: 150,\n} as const;\n\n/** Whether an action arriving at `now` is earlier than its slack allows; `due` undefined = never taken, due at once. */\nexport function cadenceEarly(now: number, due: number | undefined, slack: number): boolean {\n return due !== undefined && now < due - slack;\n}\n\n/** When the action after one accepted at `now` is due: a whole interval after the later of `now` and `due`. */\nexport function cadenceNext(now: number, due: number | undefined, interval: number): number {\n return Math.max(now, due ?? now) + interval;\n}\n"],"names":["CADENCE","cadenceEarly","now","due","slack","cadenceNext","interval"],"mappings":"AAOO,MAAMA,IAAU;AAAA;AAAA,EAErB,kBAAkB;AAAA;AAAA,EAElB,aAAa;AAAA;AAAA,EAEb,iBAAiB;AAAA;AAAA,EAEjB,eAAe;AACjB;AAGO,SAASC,EAAaC,GAAaC,GAAyBC,GAAwB;AACzF,SAAOD,MAAQ,UAAaD,IAAMC,IAAMC;AAC1C;AAGO,SAASC,EAAYH,GAAaC,GAAyBG,GAA0B;AAC1F,SAAO,KAAK,IAAIJ,GAAKC,KAAOD,CAAG,IAAII;AACrC;"}