@lampdotdev/lamp 0.0.0-stage → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (464) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +357 -2
  3. package/Resources/Devices/roku.json +410 -0
  4. package/Resources/Devices/xbox-one.json +367 -0
  5. package/Resources/GameKit/NOTICE.md +63 -0
  6. package/Resources/GameKit/apple/LampPage.swift +1646 -0
  7. package/Resources/GameKit/apple/LampPlayer.swift +46 -0
  8. package/Resources/GameKit/apple/LampWebApp.swift +63 -0
  9. package/Resources/GameKit/apple/LampWebGame.swift +87 -0
  10. package/Resources/GameKit/apple/Native.swift +12 -0
  11. package/Resources/GameKit/build.sh +55 -0
  12. package/Resources/GameKit/lamp_game.h +116 -0
  13. package/Resources/GameKit/lamp_host.h +19 -0
  14. package/Resources/GameKit/lamp_kit.cpp +61 -0
  15. package/Resources/GameKit/lamp_sound.cpp +455 -0
  16. package/Resources/GameKit/lamp_xbox.cpp +374 -0
  17. package/Resources/GameKit/libraries/HandmadeMath.h +3940 -0
  18. package/Resources/GameKit/libraries/cgltf.h +7240 -0
  19. package/Resources/GameKit/libraries/dr_mp3.h +5430 -0
  20. package/Resources/GameKit/libraries/dr_wav.h +9209 -0
  21. package/Resources/GameKit/libraries/sokol_debugtext.h +4754 -0
  22. package/Resources/GameKit/libraries/sokol_gfx.h +29558 -0
  23. package/Resources/GameKit/libraries/sokol_gl.h +4538 -0
  24. package/Resources/GameKit/libraries/sokol_log.h +335 -0
  25. package/Resources/GameKit/libraries/sokol_shape.h +1531 -0
  26. package/Resources/GameKit/libraries/stb_image.h +7988 -0
  27. package/Resources/GameKit/libraries/stb_vorbis.c +5584 -0
  28. package/Resources/GameKit/scaffold/Game/game.cpp +142 -0
  29. package/Resources/GameKit/scaffold/Game/scene.glsl +49 -0
  30. package/Resources/GameKit/scaffold-web/Web/game.js +31 -0
  31. package/Resources/GameKit/scaffold-web/Web/index.html +13 -0
  32. package/Resources/GameKit/scaffold-web/Web/making.js +33 -0
  33. package/Resources/GameKit/scaffold-web/Web/world.js +33 -0
  34. package/Resources/GameKit/web/lamp-game.js +3036 -0
  35. package/Resources/GameKit/web/lamp-phaser.js +128 -0
  36. package/Resources/WebHost/App.exe +0 -0
  37. package/Resources/WebHost/Microsoft.Web.WebView2.Core.dll +0 -0
  38. package/Resources/WebHost/WebView2-LICENSE.txt +27 -0
  39. package/Resources/WebHost/lamp_controllers.h +1506 -0
  40. package/Resources/WebHost/lamp_web_host.cpp +1173 -0
  41. package/Resources/WebLibraries/earlier.txt +318 -0
  42. package/Resources/WebLibraries/phaser/LICENSE.md +20 -0
  43. package/Resources/WebLibraries/phaser/NOTICE.md +2 -0
  44. package/Resources/WebLibraries/phaser/phaser.esm.min.js +1 -0
  45. package/Resources/WebLibraries/qrcode/LICENSE +21 -0
  46. package/Resources/WebLibraries/qrcode/NOTICE.md +4 -0
  47. package/Resources/WebLibraries/qrcode/qrcode.js +2297 -0
  48. package/Resources/WebLibraries/rapier/LICENSE +201 -0
  49. package/Resources/WebLibraries/rapier/NOTICE.md +3 -0
  50. package/Resources/WebLibraries/rapier/rapier.js +2 -0
  51. package/Resources/WebLibraries/three/LICENSE +21 -0
  52. package/Resources/WebLibraries/three/NOTICE.md +4 -0
  53. package/Resources/WebLibraries/three/addons/animation/AnimationClipCreator.js +168 -0
  54. package/Resources/WebLibraries/three/addons/animation/CCDIKSolver.js +607 -0
  55. package/Resources/WebLibraries/three/addons/controls/ArcballControls.js +3543 -0
  56. package/Resources/WebLibraries/three/addons/controls/DragControls.js +452 -0
  57. package/Resources/WebLibraries/three/addons/controls/FirstPersonControls.js +512 -0
  58. package/Resources/WebLibraries/three/addons/controls/FlyControls.js +384 -0
  59. package/Resources/WebLibraries/three/addons/controls/MapControls.js +116 -0
  60. package/Resources/WebLibraries/three/addons/controls/OrbitControls.js +1985 -0
  61. package/Resources/WebLibraries/three/addons/controls/PointerLockControls.js +264 -0
  62. package/Resources/WebLibraries/three/addons/controls/TrackballControls.js +1106 -0
  63. package/Resources/WebLibraries/three/addons/controls/TransformControls.js +2003 -0
  64. package/Resources/WebLibraries/three/addons/curves/CurveExtras.js +694 -0
  65. package/Resources/WebLibraries/three/addons/curves/NURBSCurve.js +169 -0
  66. package/Resources/WebLibraries/three/addons/curves/NURBSSurface.js +98 -0
  67. package/Resources/WebLibraries/three/addons/curves/NURBSUtils.js +532 -0
  68. package/Resources/WebLibraries/three/addons/curves/NURBSVolume.js +82 -0
  69. package/Resources/WebLibraries/three/addons/effects/AnaglyphEffect.js +274 -0
  70. package/Resources/WebLibraries/three/addons/effects/AsciiEffect.js +323 -0
  71. package/Resources/WebLibraries/three/addons/effects/OutlineEffect.js +489 -0
  72. package/Resources/WebLibraries/three/addons/effects/ParallaxBarrierEffect.js +155 -0
  73. package/Resources/WebLibraries/three/addons/effects/StereoEffect.js +91 -0
  74. package/Resources/WebLibraries/three/addons/environments/ColorEnvironment.js +59 -0
  75. package/Resources/WebLibraries/three/addons/environments/DebugEnvironment.js +102 -0
  76. package/Resources/WebLibraries/three/addons/environments/RoomEnvironment.js +185 -0
  77. package/Resources/WebLibraries/three/addons/geometries/BoxLineGeometry.js +92 -0
  78. package/Resources/WebLibraries/three/addons/geometries/ConvexGeometry.js +72 -0
  79. package/Resources/WebLibraries/three/addons/geometries/DecalGeometry.js +420 -0
  80. package/Resources/WebLibraries/three/addons/geometries/LoftGeometry.js +355 -0
  81. package/Resources/WebLibraries/three/addons/geometries/ParametricFunctions.js +100 -0
  82. package/Resources/WebLibraries/three/addons/geometries/ParametricGeometry.js +172 -0
  83. package/Resources/WebLibraries/three/addons/geometries/RoundedBoxGeometry.js +216 -0
  84. package/Resources/WebLibraries/three/addons/geometries/TeapotGeometry.js +689 -0
  85. package/Resources/WebLibraries/three/addons/geometries/TextGeometry.js +102 -0
  86. package/Resources/WebLibraries/three/addons/helpers/AnimationPathHelper.js +302 -0
  87. package/Resources/WebLibraries/three/addons/helpers/LightProbeGridHelper.js +166 -0
  88. package/Resources/WebLibraries/three/addons/helpers/LightProbeGridHelperWebGL.js +221 -0
  89. package/Resources/WebLibraries/three/addons/helpers/LightProbeHelper.js +166 -0
  90. package/Resources/WebLibraries/three/addons/helpers/LightProbeHelperGPU.js +102 -0
  91. package/Resources/WebLibraries/three/addons/helpers/OctreeHelper.js +109 -0
  92. package/Resources/WebLibraries/three/addons/helpers/PositionalAudioHelper.js +169 -0
  93. package/Resources/WebLibraries/three/addons/helpers/RapierHelper.js +59 -0
  94. package/Resources/WebLibraries/three/addons/helpers/RectAreaLightHelper.js +118 -0
  95. package/Resources/WebLibraries/three/addons/helpers/TextureHelper.js +265 -0
  96. package/Resources/WebLibraries/three/addons/helpers/TextureHelperGPU.js +214 -0
  97. package/Resources/WebLibraries/three/addons/helpers/VertexNormalsHelper.js +155 -0
  98. package/Resources/WebLibraries/three/addons/helpers/VertexTangentsHelper.js +133 -0
  99. package/Resources/WebLibraries/three/addons/helpers/ViewHelper.js +529 -0
  100. package/Resources/WebLibraries/three/addons/interactive/HTMLMesh.js +617 -0
  101. package/Resources/WebLibraries/three/addons/interactive/InteractiveGroup.js +224 -0
  102. package/Resources/WebLibraries/three/addons/interactive/SelectionBox.js +324 -0
  103. package/Resources/WebLibraries/three/addons/interactive/SelectionHelper.js +150 -0
  104. package/Resources/WebLibraries/three/addons/libs/fflate.module.js +2699 -0
  105. package/Resources/WebLibraries/three/addons/libs/lil-gui.module.min.js +8 -0
  106. package/Resources/WebLibraries/three/addons/libs/meshopt_decoder.module.js +196 -0
  107. package/Resources/WebLibraries/three/addons/libs/potpack.module.js +125 -0
  108. package/Resources/WebLibraries/three/addons/libs/stats.module.js +167 -0
  109. package/Resources/WebLibraries/three/addons/libs/tween.module.js +876 -0
  110. package/Resources/WebLibraries/three/addons/lighting/ClusteredLighting.js +153 -0
  111. package/Resources/WebLibraries/three/addons/lighting/DynamicLighting.js +82 -0
  112. package/Resources/WebLibraries/three/addons/lighting/LightMapper.js +295 -0
  113. package/Resources/WebLibraries/three/addons/lighting/LightProbeGrid.js +923 -0
  114. package/Resources/WebLibraries/three/addons/lighting/LightProbeGridUtils.js +107 -0
  115. package/Resources/WebLibraries/three/addons/lighting/LightProbeGridWebGL.js +846 -0
  116. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGIConeTracer.js +243 -0
  117. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGIIrradiance.js +43 -0
  118. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGINode.js +617 -0
  119. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGISceneCollector.js +793 -0
  120. package/Resources/WebLibraries/three/addons/lighting/vxgi/VXGIVolume.js +1440 -0
  121. package/Resources/WebLibraries/three/addons/lights/LightProbeGenerator.js +337 -0
  122. package/Resources/WebLibraries/three/addons/lights/RectAreaLightTexturesLib.js +127 -0
  123. package/Resources/WebLibraries/three/addons/lights/RectAreaLightUniformsLib.js +40 -0
  124. package/Resources/WebLibraries/three/addons/lights/SunLight.js +100 -0
  125. package/Resources/WebLibraries/three/addons/lights/SunLightNode.js +56 -0
  126. package/Resources/WebLibraries/three/addons/lights/SunLightShadow.js +319 -0
  127. package/Resources/WebLibraries/three/addons/lights/SunShadowNode.js +312 -0
  128. package/Resources/WebLibraries/three/addons/lines/Line2.js +56 -0
  129. package/Resources/WebLibraries/three/addons/lines/LineGeometry.js +157 -0
  130. package/Resources/WebLibraries/three/addons/lines/LineMaterial.js +742 -0
  131. package/Resources/WebLibraries/three/addons/lines/LineSegments2.js +434 -0
  132. package/Resources/WebLibraries/three/addons/lines/LineSegmentsGeometry.js +298 -0
  133. package/Resources/WebLibraries/three/addons/lines/Wireframe.js +108 -0
  134. package/Resources/WebLibraries/three/addons/lines/WireframeGeometry2.js +49 -0
  135. package/Resources/WebLibraries/three/addons/lines/webgpu/Line2.js +46 -0
  136. package/Resources/WebLibraries/three/addons/lines/webgpu/LineSegments2.js +419 -0
  137. package/Resources/WebLibraries/three/addons/lines/webgpu/Wireframe.js +86 -0
  138. package/Resources/WebLibraries/three/addons/loaders/EXRLoader.js +3411 -0
  139. package/Resources/WebLibraries/three/addons/loaders/FBXLoader.js +4660 -0
  140. package/Resources/WebLibraries/three/addons/loaders/FontLoader.js +262 -0
  141. package/Resources/WebLibraries/three/addons/loaders/GLTFLoader.js +5006 -0
  142. package/Resources/WebLibraries/three/addons/loaders/HDRLoader.js +463 -0
  143. package/Resources/WebLibraries/three/addons/loaders/MTLLoader.js +601 -0
  144. package/Resources/WebLibraries/three/addons/loaders/OBJLoader.js +961 -0
  145. package/Resources/WebLibraries/three/addons/loaders/PLYLoader.js +992 -0
  146. package/Resources/WebLibraries/three/addons/loaders/RGBELoader.js +18 -0
  147. package/Resources/WebLibraries/three/addons/loaders/STLLoader.js +423 -0
  148. package/Resources/WebLibraries/three/addons/loaders/SVGLoader.js +3297 -0
  149. package/Resources/WebLibraries/three/addons/loaders/TGALoader.js +539 -0
  150. package/Resources/WebLibraries/three/addons/loaders/UltraHDRLoader.js +757 -0
  151. package/Resources/WebLibraries/three/addons/math/Capsule.js +159 -0
  152. package/Resources/WebLibraries/three/addons/math/ColorConverter.js +62 -0
  153. package/Resources/WebLibraries/three/addons/math/ColorSpaces.js +147 -0
  154. package/Resources/WebLibraries/three/addons/math/ConvexHull.js +1695 -0
  155. package/Resources/WebLibraries/three/addons/math/ImprovedNoise.js +88 -0
  156. package/Resources/WebLibraries/three/addons/math/Lut.js +317 -0
  157. package/Resources/WebLibraries/three/addons/math/MeshSurfaceSampler.js +315 -0
  158. package/Resources/WebLibraries/three/addons/math/OBB.js +535 -0
  159. package/Resources/WebLibraries/three/addons/math/Octree.js +825 -0
  160. package/Resources/WebLibraries/three/addons/math/SimplexNoise.js +470 -0
  161. package/Resources/WebLibraries/three/addons/misc/ConvexObjectBreaker.js +539 -0
  162. package/Resources/WebLibraries/three/addons/misc/GPUComputationRenderer.js +508 -0
  163. package/Resources/WebLibraries/three/addons/misc/Gyroscope.js +78 -0
  164. package/Resources/WebLibraries/three/addons/misc/MorphAnimMesh.js +119 -0
  165. package/Resources/WebLibraries/three/addons/misc/MorphBlendMesh.js +425 -0
  166. package/Resources/WebLibraries/three/addons/misc/ProgressiveLightMap.js +370 -0
  167. package/Resources/WebLibraries/three/addons/misc/ProgressiveLightMapGPU.js +322 -0
  168. package/Resources/WebLibraries/three/addons/misc/RollerCoaster.js +661 -0
  169. package/Resources/WebLibraries/three/addons/misc/Sculptor.js +1679 -0
  170. package/Resources/WebLibraries/three/addons/misc/SculptorMesh.js +1504 -0
  171. package/Resources/WebLibraries/three/addons/misc/SculptorTools.js +1180 -0
  172. package/Resources/WebLibraries/three/addons/misc/SculptorUtils.js +345 -0
  173. package/Resources/WebLibraries/three/addons/misc/TileCreasedNormalsPlugin.js +270 -0
  174. package/Resources/WebLibraries/three/addons/misc/TubePainter.js +611 -0
  175. package/Resources/WebLibraries/three/addons/misc/Volume.js +520 -0
  176. package/Resources/WebLibraries/three/addons/misc/VolumeSlice.js +275 -0
  177. package/Resources/WebLibraries/three/addons/modifiers/CurveModifier.js +374 -0
  178. package/Resources/WebLibraries/three/addons/modifiers/CurveModifierGPU.js +256 -0
  179. package/Resources/WebLibraries/three/addons/modifiers/EdgeSplitModifier.js +299 -0
  180. package/Resources/WebLibraries/three/addons/modifiers/TessellateModifier.js +340 -0
  181. package/Resources/WebLibraries/three/addons/objects/GroundedSkybox.js +69 -0
  182. package/Resources/WebLibraries/three/addons/objects/Lensflare.js +489 -0
  183. package/Resources/WebLibraries/three/addons/objects/LensflareMesh.js +376 -0
  184. package/Resources/WebLibraries/three/addons/objects/MarchingCubes.js +1261 -0
  185. package/Resources/WebLibraries/three/addons/objects/Reflector.js +390 -0
  186. package/Resources/WebLibraries/three/addons/objects/ReflectorForSSRPass.js +393 -0
  187. package/Resources/WebLibraries/three/addons/objects/Refractor.js +389 -0
  188. package/Resources/WebLibraries/three/addons/objects/ShadowMesh.js +130 -0
  189. package/Resources/WebLibraries/three/addons/objects/Sky.js +342 -0
  190. package/Resources/WebLibraries/three/addons/objects/SkyMesh.js +403 -0
  191. package/Resources/WebLibraries/three/addons/objects/Water.js +374 -0
  192. package/Resources/WebLibraries/three/addons/objects/Water2.js +403 -0
  193. package/Resources/WebLibraries/three/addons/objects/Water2Mesh.js +199 -0
  194. package/Resources/WebLibraries/three/addons/objects/WaterMesh.js +194 -0
  195. package/Resources/WebLibraries/three/addons/postprocessing/AfterimagePass.js +187 -0
  196. package/Resources/WebLibraries/three/addons/postprocessing/BloomPass.js +274 -0
  197. package/Resources/WebLibraries/three/addons/postprocessing/BokehPass.js +218 -0
  198. package/Resources/WebLibraries/three/addons/postprocessing/ClearPass.js +97 -0
  199. package/Resources/WebLibraries/three/addons/postprocessing/CubeTexturePass.js +146 -0
  200. package/Resources/WebLibraries/three/addons/postprocessing/DotScreenPass.js +114 -0
  201. package/Resources/WebLibraries/three/addons/postprocessing/EffectComposer.js +365 -0
  202. package/Resources/WebLibraries/three/addons/postprocessing/FXAAPass.js +40 -0
  203. package/Resources/WebLibraries/three/addons/postprocessing/FilmPass.js +113 -0
  204. package/Resources/WebLibraries/three/addons/postprocessing/GTAOPass.js +735 -0
  205. package/Resources/WebLibraries/three/addons/postprocessing/GlitchPass.js +177 -0
  206. package/Resources/WebLibraries/three/addons/postprocessing/HalftonePass.js +134 -0
  207. package/Resources/WebLibraries/three/addons/postprocessing/LUTPass.js +138 -0
  208. package/Resources/WebLibraries/three/addons/postprocessing/MaskPass.js +195 -0
  209. package/Resources/WebLibraries/three/addons/postprocessing/OutlinePass.js +776 -0
  210. package/Resources/WebLibraries/three/addons/postprocessing/OutputPass.js +148 -0
  211. package/Resources/WebLibraries/three/addons/postprocessing/Pass.js +191 -0
  212. package/Resources/WebLibraries/three/addons/postprocessing/RenderPass.js +193 -0
  213. package/Resources/WebLibraries/three/addons/postprocessing/RenderPixelatedPass.js +314 -0
  214. package/Resources/WebLibraries/three/addons/postprocessing/RenderTransitionPass.js +267 -0
  215. package/Resources/WebLibraries/three/addons/postprocessing/SAOPass.js +407 -0
  216. package/Resources/WebLibraries/three/addons/postprocessing/SMAAPass.js +230 -0
  217. package/Resources/WebLibraries/three/addons/postprocessing/SSAARenderPass.js +313 -0
  218. package/Resources/WebLibraries/three/addons/postprocessing/SSAOPass.js +527 -0
  219. package/Resources/WebLibraries/three/addons/postprocessing/SSRPass.js +858 -0
  220. package/Resources/WebLibraries/three/addons/postprocessing/SavePass.js +132 -0
  221. package/Resources/WebLibraries/three/addons/postprocessing/ShaderPass.js +135 -0
  222. package/Resources/WebLibraries/three/addons/postprocessing/TAARenderPass.js +243 -0
  223. package/Resources/WebLibraries/three/addons/postprocessing/TexturePass.js +131 -0
  224. package/Resources/WebLibraries/three/addons/postprocessing/UnrealBloomPass.js +541 -0
  225. package/Resources/WebLibraries/three/addons/shaders/ACESFilmicToneMappingShader.js +96 -0
  226. package/Resources/WebLibraries/three/addons/shaders/AfterimageShader.js +63 -0
  227. package/Resources/WebLibraries/three/addons/shaders/BasicShader.js +36 -0
  228. package/Resources/WebLibraries/three/addons/shaders/BleachBypassShader.js +68 -0
  229. package/Resources/WebLibraries/three/addons/shaders/BlendShader.js +56 -0
  230. package/Resources/WebLibraries/three/addons/shaders/BokehShader.js +151 -0
  231. package/Resources/WebLibraries/three/addons/shaders/BokehShader2.js +404 -0
  232. package/Resources/WebLibraries/three/addons/shaders/BrightnessContrastShader.js +62 -0
  233. package/Resources/WebLibraries/three/addons/shaders/ColorCorrectionShader.js +59 -0
  234. package/Resources/WebLibraries/three/addons/shaders/ColorifyShader.js +57 -0
  235. package/Resources/WebLibraries/three/addons/shaders/ConvolutionShader.js +74 -0
  236. package/Resources/WebLibraries/three/addons/shaders/CopyShader.js +52 -0
  237. package/Resources/WebLibraries/three/addons/shaders/DOFMipMapShader.js +63 -0
  238. package/Resources/WebLibraries/three/addons/shaders/DepthLimitedBlurShader.js +183 -0
  239. package/Resources/WebLibraries/three/addons/shaders/DigitalGlitch.js +104 -0
  240. package/Resources/WebLibraries/three/addons/shaders/DotScreenShader.js +75 -0
  241. package/Resources/WebLibraries/three/addons/shaders/ExposureShader.js +51 -0
  242. package/Resources/WebLibraries/three/addons/shaders/FXAAShader.js +298 -0
  243. package/Resources/WebLibraries/three/addons/shaders/FilmShader.js +73 -0
  244. package/Resources/WebLibraries/three/addons/shaders/FocusShader.js +94 -0
  245. package/Resources/WebLibraries/three/addons/shaders/FreiChenShader.js +103 -0
  246. package/Resources/WebLibraries/three/addons/shaders/GTAOShader.js +437 -0
  247. package/Resources/WebLibraries/three/addons/shaders/GammaCorrectionShader.js +52 -0
  248. package/Resources/WebLibraries/three/addons/shaders/HalftoneShader.js +332 -0
  249. package/Resources/WebLibraries/three/addons/shaders/HorizontalBlurShader.js +68 -0
  250. package/Resources/WebLibraries/three/addons/shaders/HorizontalTiltShiftShader.js +70 -0
  251. package/Resources/WebLibraries/three/addons/shaders/HueSaturationShader.js +74 -0
  252. package/Resources/WebLibraries/three/addons/shaders/KaleidoShader.js +65 -0
  253. package/Resources/WebLibraries/three/addons/shaders/LuminosityHighPassShader.js +68 -0
  254. package/Resources/WebLibraries/three/addons/shaders/LuminosityShader.js +54 -0
  255. package/Resources/WebLibraries/three/addons/shaders/MirrorShader.js +62 -0
  256. package/Resources/WebLibraries/three/addons/shaders/NormalMapShader.js +60 -0
  257. package/Resources/WebLibraries/three/addons/shaders/OutputShader.js +103 -0
  258. package/Resources/WebLibraries/three/addons/shaders/PoissonDenoiseShader.js +230 -0
  259. package/Resources/WebLibraries/three/addons/shaders/RGBShiftShader.js +61 -0
  260. package/Resources/WebLibraries/three/addons/shaders/SAOShader.js +201 -0
  261. package/Resources/WebLibraries/three/addons/shaders/SMAAShader.js +489 -0
  262. package/Resources/WebLibraries/three/addons/shaders/SSAOShader.js +331 -0
  263. package/Resources/WebLibraries/three/addons/shaders/SSRShader.js +396 -0
  264. package/Resources/WebLibraries/three/addons/shaders/SepiaShader.js +57 -0
  265. package/Resources/WebLibraries/three/addons/shaders/SobelOperatorShader.js +98 -0
  266. package/Resources/WebLibraries/three/addons/shaders/SubsurfaceScatteringShader.js +95 -0
  267. package/Resources/WebLibraries/three/addons/shaders/TechnicolorShader.js +51 -0
  268. package/Resources/WebLibraries/three/addons/shaders/ToonShader.js +349 -0
  269. package/Resources/WebLibraries/three/addons/shaders/TriangleBlurShader.js +79 -0
  270. package/Resources/WebLibraries/three/addons/shaders/UnpackDepthRGBAShader.js +60 -0
  271. package/Resources/WebLibraries/three/addons/shaders/VelocityShader.js +137 -0
  272. package/Resources/WebLibraries/three/addons/shaders/VerticalBlurShader.js +66 -0
  273. package/Resources/WebLibraries/three/addons/shaders/VerticalTiltShiftShader.js +70 -0
  274. package/Resources/WebLibraries/three/addons/shaders/VignetteShader.js +56 -0
  275. package/Resources/WebLibraries/three/addons/shaders/VolumeShader.js +285 -0
  276. package/Resources/WebLibraries/three/addons/shaders/WaterRefractionShader.js +106 -0
  277. package/Resources/WebLibraries/three/addons/textures/FlakesTexture.js +54 -0
  278. package/Resources/WebLibraries/three/addons/tsl/WebGLNodesHandler.js +607 -0
  279. package/Resources/WebLibraries/three/addons/tsl/display/AfterImageNode.js +256 -0
  280. package/Resources/WebLibraries/three/addons/tsl/display/AnaglyphPassNode.js +550 -0
  281. package/Resources/WebLibraries/three/addons/tsl/display/BilateralBlurNode.js +377 -0
  282. package/Resources/WebLibraries/three/addons/tsl/display/BleachBypass.js +33 -0
  283. package/Resources/WebLibraries/three/addons/tsl/display/BloomNode.js +599 -0
  284. package/Resources/WebLibraries/three/addons/tsl/display/CRT.js +150 -0
  285. package/Resources/WebLibraries/three/addons/tsl/display/ChromaticAberrationNode.js +163 -0
  286. package/Resources/WebLibraries/three/addons/tsl/display/DenoiseNode.js +354 -0
  287. package/Resources/WebLibraries/three/addons/tsl/display/DepthOfFieldNode.js +572 -0
  288. package/Resources/WebLibraries/three/addons/tsl/display/DotScreenNode.js +104 -0
  289. package/Resources/WebLibraries/three/addons/tsl/display/FSR1Node.js +481 -0
  290. package/Resources/WebLibraries/three/addons/tsl/display/FXAANode.js +357 -0
  291. package/Resources/WebLibraries/three/addons/tsl/display/FilmNode.js +101 -0
  292. package/Resources/WebLibraries/three/addons/tsl/display/GTAONode.js +727 -0
  293. package/Resources/WebLibraries/three/addons/tsl/display/GaussianBlurNode.js +412 -0
  294. package/Resources/WebLibraries/three/addons/tsl/display/GodraysNode.js +604 -0
  295. package/Resources/WebLibraries/three/addons/tsl/display/ImportanceSampledEnvironment.js +564 -0
  296. package/Resources/WebLibraries/three/addons/tsl/display/LensflareNode.js +282 -0
  297. package/Resources/WebLibraries/three/addons/tsl/display/Lut3DNode.js +109 -0
  298. package/Resources/WebLibraries/three/addons/tsl/display/MotionBlur.js +33 -0
  299. package/Resources/WebLibraries/three/addons/tsl/display/OITPassNode.js +341 -0
  300. package/Resources/WebLibraries/three/addons/tsl/display/OutlineNode.js +916 -0
  301. package/Resources/WebLibraries/three/addons/tsl/display/ParallaxBarrierPassNode.js +90 -0
  302. package/Resources/WebLibraries/three/addons/tsl/display/PixelationPassNode.js +227 -0
  303. package/Resources/WebLibraries/three/addons/tsl/display/RGBShiftNode.js +96 -0
  304. package/Resources/WebLibraries/three/addons/tsl/display/RecurrentDenoiseNode.js +812 -0
  305. package/Resources/WebLibraries/three/addons/tsl/display/RetroPassNode.js +263 -0
  306. package/Resources/WebLibraries/three/addons/tsl/display/SMAANode.js +729 -0
  307. package/Resources/WebLibraries/three/addons/tsl/display/SSAAPassNode.js +344 -0
  308. package/Resources/WebLibraries/three/addons/tsl/display/SSAONode.js +422 -0
  309. package/Resources/WebLibraries/three/addons/tsl/display/SSGINode.js +698 -0
  310. package/Resources/WebLibraries/three/addons/tsl/display/SSRNode.js +1430 -0
  311. package/Resources/WebLibraries/three/addons/tsl/display/SSSNode.js +479 -0
  312. package/Resources/WebLibraries/three/addons/tsl/display/Sepia.js +24 -0
  313. package/Resources/WebLibraries/three/addons/tsl/display/Shape.js +29 -0
  314. package/Resources/WebLibraries/three/addons/tsl/display/SharpenNode.js +284 -0
  315. package/Resources/WebLibraries/three/addons/tsl/display/SobelOperatorNode.js +168 -0
  316. package/Resources/WebLibraries/three/addons/tsl/display/StereoCompositePassNode.js +190 -0
  317. package/Resources/WebLibraries/three/addons/tsl/display/StereoPassNode.js +117 -0
  318. package/Resources/WebLibraries/three/addons/tsl/display/TAAUNode.js +700 -0
  319. package/Resources/WebLibraries/three/addons/tsl/display/TRAANode.js +609 -0
  320. package/Resources/WebLibraries/three/addons/tsl/display/TemporalReprojectNode.js +1024 -0
  321. package/Resources/WebLibraries/three/addons/tsl/display/TransitionNode.js +141 -0
  322. package/Resources/WebLibraries/three/addons/tsl/display/boxBlur.js +65 -0
  323. package/Resources/WebLibraries/three/addons/tsl/display/depthAwareBlend.js +80 -0
  324. package/Resources/WebLibraries/three/addons/tsl/display/depthAwareBlur.js +63 -0
  325. package/Resources/WebLibraries/three/addons/tsl/display/hashBlur.js +55 -0
  326. package/Resources/WebLibraries/three/addons/tsl/display/lensDirt.js +30 -0
  327. package/Resources/WebLibraries/three/addons/tsl/display/radialBlur.js +68 -0
  328. package/Resources/WebLibraries/three/addons/tsl/lighting/ClusteredLightsNode.js +994 -0
  329. package/Resources/WebLibraries/three/addons/tsl/lighting/DynamicLightsNode.js +201 -0
  330. package/Resources/WebLibraries/three/addons/tsl/lighting/LightProbeGridNode.js +183 -0
  331. package/Resources/WebLibraries/three/addons/tsl/lighting/data/AmbientLightDataNode.js +67 -0
  332. package/Resources/WebLibraries/three/addons/tsl/lighting/data/DirectionalLightDataNode.js +117 -0
  333. package/Resources/WebLibraries/three/addons/tsl/lighting/data/HemisphereLightDataNode.js +105 -0
  334. package/Resources/WebLibraries/three/addons/tsl/lighting/data/PointLightDataNode.js +140 -0
  335. package/Resources/WebLibraries/three/addons/tsl/lighting/data/SpotLightDataNode.js +167 -0
  336. package/Resources/WebLibraries/three/addons/tsl/math/Bayer.js +84 -0
  337. package/Resources/WebLibraries/three/addons/tsl/math/curlNoise.js +107 -0
  338. package/Resources/WebLibraries/three/addons/tsl/math/voronoiNoise.js +112 -0
  339. package/Resources/WebLibraries/three/addons/tsl/shadows/TileShadowNode.js +542 -0
  340. package/Resources/WebLibraries/three/addons/tsl/shadows/TileShadowNodeHelper.js +212 -0
  341. package/Resources/WebLibraries/three/addons/tsl/utils/GroundedSkybox.js +62 -0
  342. package/Resources/WebLibraries/three/addons/tsl/utils/RNoise.js +51 -0
  343. package/Resources/WebLibraries/three/addons/tsl/utils/Raymarching.js +70 -0
  344. package/Resources/WebLibraries/three/addons/tsl/utils/SoftParticles.js +53 -0
  345. package/Resources/WebLibraries/three/addons/tsl/utils/SpecularHelpers.js +302 -0
  346. package/Resources/WebLibraries/three/addons/tsl/utils/TAAUtils.js +184 -0
  347. package/Resources/WebLibraries/three/addons/utils/BufferGeometryUtils.js +1523 -0
  348. package/Resources/WebLibraries/three/addons/utils/CameraUtils.js +82 -0
  349. package/Resources/WebLibraries/three/addons/utils/ColorUtils.js +76 -0
  350. package/Resources/WebLibraries/three/addons/utils/GaussianSplatUtils.js +229 -0
  351. package/Resources/WebLibraries/three/addons/utils/GeometryCompressionUtils.js +547 -0
  352. package/Resources/WebLibraries/three/addons/utils/GeometryUtils.js +226 -0
  353. package/Resources/WebLibraries/three/addons/utils/LDrawUtils.js +211 -0
  354. package/Resources/WebLibraries/three/addons/utils/SceneOptimizer.js +459 -0
  355. package/Resources/WebLibraries/three/addons/utils/SceneUtils.js +363 -0
  356. package/Resources/WebLibraries/three/addons/utils/ShadowMapViewer.js +244 -0
  357. package/Resources/WebLibraries/three/addons/utils/ShadowMapViewerGPU.js +233 -0
  358. package/Resources/WebLibraries/three/addons/utils/SkeletonUtils.js +496 -0
  359. package/Resources/WebLibraries/three/addons/utils/SortUtils.js +175 -0
  360. package/Resources/WebLibraries/three/addons/utils/UVsDebug.js +173 -0
  361. package/Resources/WebLibraries/three/addons/utils/WebGLTextureUtils.js +115 -0
  362. package/Resources/WebLibraries/three/addons/utils/WebGPUTextureUtils.js +81 -0
  363. package/Resources/WebLibraries/three/addons/utils/WorkerPool.js +167 -0
  364. package/Resources/WebLibraries/three/addons/utils/unwrapUVs.js +865 -0
  365. package/Resources/WebLibraries/three/three.core.js +61162 -0
  366. package/Resources/WebLibraries/three/three.module.js +19537 -0
  367. package/Resources/WebLibraries/three/three.tsl.js +41903 -0
  368. package/Resources/WebLibraries/three/three.webgpu.js +53714 -0
  369. package/Resources/skills/2d-game/SKILL.md +305 -0
  370. package/Resources/skills/3d-game/SKILL.md +274 -0
  371. package/Resources/skills/accounts/SKILL.md +41 -0
  372. package/Resources/skills/android-design/SKILL.md +159 -0
  373. package/Resources/skills/app-ai/SKILL.md +154 -0
  374. package/Resources/skills/app-intents/SKILL.md +31 -0
  375. package/Resources/skills/app-kinds/SKILL.md +122 -0
  376. package/Resources/skills/apple-watch/SKILL.md +43 -0
  377. package/Resources/skills/assets/SKILL.md +183 -0
  378. package/Resources/skills/building-blocks/SKILL.md +97 -0
  379. package/Resources/skills/device-control/SKILL.md +56 -0
  380. package/Resources/skills/device-integrations/SKILL.md +278 -0
  381. package/Resources/skills/finder-files/SKILL.md +19 -0
  382. package/Resources/skills/game/SKILL.md +414 -0
  383. package/Resources/skills/game-3d/SKILL.md +12 -0
  384. package/Resources/skills/game-art/SKILL.md +11 -0
  385. package/Resources/skills/game-feel/SKILL.md +13 -0
  386. package/Resources/skills/game-levels/SKILL.md +12 -0
  387. package/Resources/skills/game-live/SKILL.md +12 -0
  388. package/Resources/skills/game-physics/SKILL.md +13 -0
  389. package/Resources/skills/game-playtest/SKILL.md +26 -0
  390. package/Resources/skills/game-ui/SKILL.md +278 -0
  391. package/Resources/skills/github-projects/SKILL.md +59 -0
  392. package/Resources/skills/icloud-sync/SKILL.md +30 -0
  393. package/Resources/skills/iphone-hardware/SKILL.md +57 -0
  394. package/Resources/skills/iphone-widgets/SKILL.md +53 -0
  395. package/Resources/skills/lg-developer-mode/SKILL.md +152 -0
  396. package/Resources/skills/mac-widgets/SKILL.md +60 -0
  397. package/Resources/skills/maps/SKILL.md +8 -0
  398. package/Resources/skills/menu-bar/SKILL.md +36 -0
  399. package/Resources/skills/more-apps/SKILL.md +53 -0
  400. package/Resources/skills/phone-controllers/SKILL.md +79 -0
  401. package/Resources/skills/phone-pages/SKILL.md +173 -0
  402. package/Resources/skills/roku-developer-mode/SKILL.md +106 -0
  403. package/Resources/skills/scene-3d/SKILL.md +27 -0
  404. package/Resources/skills/scripting/SKILL.md +17 -0
  405. package/Resources/skills/share-extension/SKILL.md +22 -0
  406. package/Resources/skills/tv-design/SKILL.md +72 -0
  407. package/Resources/skills/web-app/SKILL.md +136 -0
  408. package/Resources/skills/web-game/SKILL.md +95 -0
  409. package/Resources/skills/web-or-native/SKILL.md +103 -0
  410. package/Resources/skills/wish-links/SKILL.md +156 -0
  411. package/THIRD_PARTY_NOTICES.md +170 -0
  412. package/cli/android.mjs +941 -0
  413. package/cli/browser.mjs +268 -0
  414. package/cli/calls.mjs +390 -0
  415. package/cli/dev.mjs +773 -0
  416. package/cli/devices.mjs +984 -0
  417. package/cli/lamp.mjs +1016 -0
  418. package/cli/lampapp.mjs +332 -0
  419. package/cli/lamps/android-host/LampPageGateway.kt +345 -0
  420. package/cli/lamps/android-host/app/build.gradle.kts +40 -0
  421. package/cli/lamps/android-host/build.gradle.kts +4 -0
  422. package/cli/lamps/android-host/game/AndroidManifest.xml +33 -0
  423. package/cli/lamps/android-host/game/MainActivity.kt +173 -0
  424. package/cli/lamps/android-host/gradle/libs.versions.toml +39 -0
  425. package/cli/lamps/android-host/gradle.properties +8 -0
  426. package/cli/lamps/android-host/page/AndroidManifest.xml +35 -0
  427. package/cli/lamps/android-host/page/MainActivity.kt +193 -0
  428. package/cli/lamps/android-host/settings.gradle.kts +30 -0
  429. package/cli/lamps/android-host/tools.json +43 -0
  430. package/cli/lamps/lamp-controllers.js +2 -0
  431. package/cli/lamps/lamp-for-games.js +6863 -0
  432. package/cli/lamps/lamp.js +7023 -0
  433. package/cli/lamps/mac-starter/.lamp/meta.json +1 -0
  434. package/cli/lamps/mac-starter/APP.md +12 -0
  435. package/cli/lamps/mac-starter/Package.swift +8 -0
  436. package/cli/lamps/mac-starter/Sources/App.swift +11 -0
  437. package/cli/lamps/mac-starter/Sources/ContentView.swift +17 -0
  438. package/cli/lamps/mac-starter/Sources/LampSupport.swift +18097 -0
  439. package/cli/lamps/mac-starter/build.sh +336 -0
  440. package/cli/lamps/mac-starter/gitignore +3 -0
  441. package/cli/lamps/mac-starter/lamp-starter.json +6 -0
  442. package/cli/lamps/web-starter/APP.md +8 -0
  443. package/cli/lamps/web-starter/app.js +1 -0
  444. package/cli/lamps/web-starter/index.html +17 -0
  445. package/cli/lamps/web-starter/lamp-starter.json +5 -0
  446. package/cli/lamps/web-starter/lamp.js +7023 -0
  447. package/cli/lamps/web-starter/style.css +25 -0
  448. package/cli/link.mjs +138 -0
  449. package/cli/mac.mjs +621 -0
  450. package/cli/mdns.mjs +205 -0
  451. package/cli/msix.mjs +413 -0
  452. package/cli/network.mjs +209 -0
  453. package/cli/outside.mjs +140 -0
  454. package/cli/page.js +165 -0
  455. package/cli/place.mjs +298 -0
  456. package/cli/png.mjs +184 -0
  457. package/cli/share.mjs +202 -0
  458. package/cli/skill/SKILL.md +255 -0
  459. package/cli/skills.mjs +71 -0
  460. package/cli/state.mjs +173 -0
  461. package/cli/update.mjs +57 -0
  462. package/package.json +20 -4
  463. package/site/lib/quota.js +59 -0
  464. package/site/lib/shared-wishes.js +996 -0
@@ -0,0 +1,3036 @@
1
+ // Lamp's game runtime for a game drawn with three.js, or with Phaser for a 2D game (lamp-phaser.js, which runs Phaser's
2
+ // scenes through this). Lamp writes this file before every build; don't edit it.
3
+ //
4
+ // The game is two functions, init and frame, given to game(): the runtime runs them about 60 times a second wherever
5
+ // the game is played (on the Mac and on an iPhone or iPad in Lamp's app around the page, on the Xbox in its web host),
6
+ // draws at 1920x1080 scaled to the screen (smaller when frames run slow there), and hands each frame the controllers
7
+ // as pads: a controller connected to the device, the keyboard as the first pad, and on a touch screen the touch
8
+ // controls it draws. Use frame.dt, random() and save()/load() from here, never requestAnimationFrame, Math.random, the
9
+ // clock or localStorage yourself. What the player is holding (a controller, and whose, the keyboard or the touch screen)
10
+ // is input(), and label() and glyph() name and draw a button for it, so a prompt never names one the player hasn't got.
11
+ //
12
+ // import * as THREE from 'three';
13
+ // import { game, BUTTON, STICK, random, sound, log, label, glyph, onInput, fixedStep } from './lamp-game.js';
14
+ // const renderer = new THREE.WebGLRenderer({ antialias: true });
15
+ // game({ renderer, async init() { … }, frame(f) { … renderer.render(scene, camera); } });
16
+
17
+ import device from './lamp-device.js';
18
+ import controllers from './lamp-controllers.js';
19
+
20
+ export const WIDTH = 1920, HEIGHT = 1080;
21
+
22
+ // The controller's buttons, as bits of a pad's down, pressed and released.
23
+ export const BUTTON = { A: 1, B: 2, X: 4, Y: 8, LB: 16, RB: 32, MENU: 64, VIEW: 128, UP: 256, DOWN: 512, LEFT: 1024, RIGHT: 2048,
24
+ LSTICK: 4096, RSTICK: 8192 };
25
+ // The sticks and triggers, for `label` and `glyph` (a pad reads them as lx, ly, rx, ry, lt and rt).
26
+ export const STICK = { LEFT: 'stick-left', RIGHT: 'stick-right' };
27
+ export const TRIGGER = { LT: 'lt', RT: 'rt' };
28
+
29
+ // The browser's own functions, kept before the runtime starts watching what the game's code leaves running (see
30
+ // "Running the game's code again"): the runtime's own listeners, timers and sound go through these, so they stay when
31
+ // the game's are taken away.
32
+ // A runtime run outside a page (Lamp's tests) may have none of them, and uses whatever the target has.
33
+ const events = globalThis.EventTarget && EventTarget.prototype;
34
+ const browser = {
35
+ listen: events ? events.addEventListener : function (...rest) { return this.addEventListener(...rest); },
36
+ unlisten: events ? events.removeEventListener : function (...rest) { return this.removeEventListener(...rest); },
37
+ setTimeout: window.setTimeout, clearTimeout: window.clearTimeout, setInterval: window.setInterval, clearInterval: window.clearInterval,
38
+ frame: window.requestAnimationFrame, cancelFrame: window.cancelAnimationFrame,
39
+ AudioContext: window.AudioContext || window.webkitAudioContext, Worker: window.Worker,
40
+ };
41
+ const listen = (target, ...rest) => browser.listen.call(target, ...rest);
42
+ const after = (ms, fn) => browser.setTimeout.call(window, fn, ms);
43
+ const nextFrame = fn => browser.frame.call(window, fn);
44
+ // A runtime run outside a page (Lamp's tests) may have no performance clock.
45
+ const clockNow = () => globalThis.performance ? performance.now() : Date.now();
46
+
47
+ // MARK: Random numbers (xorshift64*, as the C++ kit has it)
48
+
49
+ let seed = BigInt(Date.now()) | 1n;
50
+ const mask = (1n << 64n) - 1n;
51
+ // How many have been drawn, so a part that's kept rather than built again can skip the draws its build made.
52
+ let draws = 0;
53
+ export function random() {
54
+ draws++;
55
+ seed ^= seed >> 12n; seed ^= (seed << 25n) & mask; seed ^= seed >> 27n;
56
+ return Number(((seed * 0x2545F4914F6CDD1Dn) & mask) >> 40n) / 16777216;
57
+ }
58
+
59
+ // MARK: The log, saves and files
60
+
61
+ export function log(...parts) {
62
+ const line = parts.map(p => typeof p === 'string' ? p : JSON.stringify(p)).join(' ');
63
+ const said = `[${time.toFixed(2)} s] ${line}`;
64
+ // The app around the page puts its console into the app's log.
65
+ console.log(said);
66
+ }
67
+ window.addEventListener('error', e => log(`Error: ${e.message} (${(e.filename || '').split('/').pop()}:${e.lineno})`));
68
+ window.addEventListener('unhandledrejection', e => log(`Error: ${e.reason && e.reason.message || e.reason}`));
69
+
70
+ export function save(name, value) {
71
+ const text = JSON.stringify(value);
72
+ try { localStorage.setItem('lamp.save.' + name, text); } catch (e) { return false; }
73
+ return true;
74
+ }
75
+ export function load(name) {
76
+ const text = (() => { try { return localStorage.getItem('lamp.save.' + name); } catch (e) { return null; } })();
77
+ try { return text == null ? null : JSON.parse(text); } catch (e) { return null; }
78
+ }
79
+
80
+ // A file beside the page (assets/ship.glb), as bytes. Loaded through here (or texture(), model() and sound.load()),
81
+ // a file Lamp changes while the game is played can be put into it without starting again (see "Changes while it's
82
+ // played" below).
83
+ export async function asset(path) {
84
+ took(clean(path));
85
+ let answer;
86
+ try { answer = await fetch(fresh(path)); } catch (e) { log(`asset: couldn't fetch ${path}`); loadProblem(); return null; }
87
+ if (!answer.ok) { log(`asset: no ${path}`); loadProblem(answer.status); return null; }
88
+ // The body can still stop partway, after the answer's headers came.
89
+ try { return await answer.arrayBuffer(); } catch (e) { log(`asset: ${path} stopped partway`); loadProblem(); return null; }
90
+ }
91
+
92
+ // A file the game needed that didn't come, said on the page rather than only in its log, with Reload: a game that
93
+ // waits on it otherwise sits on its loading screen with nothing said. A file that isn't there (404) can be one the
94
+ // game only tries, so that's said only when the page itself is gone too: a newer version took its place.
95
+ let problemShown = false;
96
+ async function loadProblem(status) {
97
+ if (problemShown || !document.body) return;
98
+ let words = "Part of the game didn't load.";
99
+ let updated = false;
100
+ if (status === 404) {
101
+ let gone = false;
102
+ try { gone = (await fetch(new URL('./', location.href), { method: 'HEAD', cache: 'no-store' })).status === 404; } catch (e) {}
103
+ if (!gone || problemShown) return;
104
+ words = 'This game was just updated.';
105
+ updated = true;
106
+ }
107
+ problemShown = true;
108
+ const bar = document.createElement('div');
109
+ bar.setAttribute('role', 'alert');
110
+ bar.style.cssText = 'position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:2147483647;display:flex;' +
111
+ 'align-items:center;gap:12px;padding:8px 8px 8px 16px;border-radius:999px;background:rgba(20,20,22,.92);color:#fff;' +
112
+ 'font:500 14px/1.3 -apple-system,system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.35);max-width:calc(100vw - 32px)';
113
+ const text = document.createElement('span');
114
+ text.textContent = words;
115
+ const reload = document.createElement('button');
116
+ reload.type = 'button';
117
+ reload.textContent = 'Reload';
118
+ reload.style.cssText = 'border:0;border-radius:999px;padding:6px 14px;background:#fff;color:#111;font:600 13px/1 -apple-system,system-ui,sans-serif;cursor:pointer';
119
+ // Starting afresh keeps the player's place where the game saves it (`__lampReloadNow`). A page that's been replaced
120
+ // can't come back at its own address: the page around it (a Share Link's on lamp.dev) is asked to load again, which
121
+ // brings the new version; on its own, it loads again.
122
+ reload.addEventListener('click', () => {
123
+ if (!updated) return (window.__lampReloadNow || (() => location.reload()))();
124
+ if (window.top !== window) window.parent.postMessage({ lamp: 'reload-page' }, '*');
125
+ else location.reload();
126
+ });
127
+ bar.append(text, reload);
128
+ document.body.append(bar);
129
+ }
130
+
131
+ // Work done in a worker (js/city-worker.js, a module beside the page), off the frame the game draws: `message` is sent
132
+ // to it, with `transfer` (ArrayBuffers handed over rather than copied), and what it posts back first is the answer,
133
+ // after which it ends. For the pure data work of a part built in the background (see "Building in the background"):
134
+ // parsing, and the arrays of positions, normals and indices that geometry is made from, posted back with their buffers
135
+ // as transfer. The part built with it is built again when the worker's file changes.
136
+ //
137
+ // // js/city-worker.js: onmessage = e => { const positions = new Float32Array(…); postMessage({ positions }, [positions.buffer]); };
138
+ // const { positions } = await work('js/city-worker.js', { blocks: MAP.blocks });
139
+ export function work(path, message, transfer = []) {
140
+ read(clean(path));
141
+ return new Promise((done, fail) => {
142
+ const worker = new browser.Worker(`${addressOf(clean(path))}?lamp=${++versionCount}`, { type: 'module' });
143
+ worker.onmessage = e => { worker.terminate(); done(e.data); };
144
+ worker.onerror = e => { worker.terminate(); fail(new Error(`${path}: ${e.message || 'the worker failed'}`)); };
145
+ worker.postMessage(message, transfer);
146
+ });
147
+ }
148
+
149
+ // The files the game loaded through the runtime, and how the runtime puts a new copy of each in by itself.
150
+ const loaded = new Set(), swaps = new Map();
151
+ // Every file of the page this copy has read, through the runtime, fetched or started as a worker: what the running game
152
+ // holds may have come from any of them (`unusedFiles`, `waitsFor`). What a worker reads for itself the page never sees.
153
+ const everRead = new Set();
154
+ let workerStarted = false;
155
+ const clean = path => String(path).replace(/^\.?\//, '').split(/[?#]/)[0];
156
+ // Asked again after a change, a file is asked for by a new address, so nothing on the way keeps the old one.
157
+ let changes = 0;
158
+ const fresh = path => changes ? `${path}${String(path).includes('?') ? '&' : '?'}lamp=${changes}` : path;
159
+ // So is a file of the page's that the game's own code fetches (a city's building data), once anything has changed. What
160
+ // a part kept with keep() fetches as it's built is what it's built from (`read`), as a file loaded through the runtime is.
161
+ if (window.fetch) {
162
+ const fetchFile = window.fetch;
163
+ window.fetch = function (input, init) {
164
+ const url = typeof input === 'string' || input instanceof URL ? String(input) : input && input.url;
165
+ const path = url ? pathOf(url) : null;
166
+ if (path != null) read(clean(path));
167
+ if (changes && !(init && init.cache) && path != null) init = { ...init, cache: 'no-store' };
168
+ return fetchFile.call(this, input, init);
169
+ };
170
+ }
171
+
172
+ // A picture beside the page as a three.js texture, set up by `setup(texture)` (its color space, wrapping, repeat…). A
173
+ // new copy of the picture goes into the same texture while the game is played, so every material using it shows it.
174
+ export async function texture(path, setup) {
175
+ const THREE = await import('three');
176
+ const loader = new THREE.TextureLoader();
177
+ const made = await loader.loadAsync(fresh(path));
178
+ if (setup) setup(made);
179
+ took(clean(path));
180
+ swaps.set(clean(path), [...(swaps.get(clean(path)) || []), async () => {
181
+ const again = await loader.loadAsync(fresh(path));
182
+ made.image = again.image;
183
+ made.needsUpdate = true;
184
+ again.dispose();
185
+ }]);
186
+ return made;
187
+ }
188
+
189
+ // A glTF model beside the page (.glb or .gltf), as a THREE.Group holding its scene, with its animations in
190
+ // `group.animations`. A new copy of the file replaces what's inside the group while the game is played, where the group
191
+ // is: a copy made with clone(), and a mixer playing its animations, are the game's to make again in assetChanged.
192
+ export async function model(path) {
193
+ const THREE = await import('three');
194
+ const { GLTFLoader } = await import('three/addons/loaders/GLTFLoader.js');
195
+ const loader = new GLTFLoader();
196
+ const group = new THREE.Group();
197
+ const fill = gltf => { group.clear(); group.add(gltf.scene); group.animations = gltf.animations; };
198
+ fill(await loader.loadAsync(fresh(path)));
199
+ group.name = clean(path);
200
+ took(clean(path));
201
+ swaps.set(clean(path), [...(swaps.get(clean(path)) || []), async () => {
202
+ const old = [...group.children];
203
+ fill(await loader.loadAsync(fresh(path)));
204
+ for (const child of old) child.traverse(o => { o.geometry && o.geometry.dispose(); });
205
+ }]);
206
+ return group;
207
+ }
208
+
209
+ // MARK: Sound, mixed by the browser's Web Audio
210
+
211
+ const sounds = [];
212
+ let audio = null, effectsVolume = 1, musicVolume = 1, musicVoice = null, music = { gain: 0 };
213
+ // The effects playing now, so a change of the effects volume reaches them too.
214
+ const voices = new Set();
215
+ function context() {
216
+ if (!audio) { try { audio = new browser.AudioContext({ sampleRate: 48000 }); } catch (e) { return null; } }
217
+ if (audio.state === 'suspended') audio.resume();
218
+ return audio;
219
+ }
220
+ // Played as soon as the page is touched, clicked or a key pressed, for a browser that holds sound back until then.
221
+ for (const kind of ['pointerdown', 'keydown', 'touchstart']) window.addEventListener(kind, () => context(), { passive: true });
222
+
223
+ function made(samples, name) {
224
+ const s = { id: sounds.length + 1, name, samples, buffer: null };
225
+ sounds.push(s);
226
+ return s;
227
+ }
228
+ function bufferOf(s) {
229
+ const ctx = context();
230
+ if (!ctx || !s || !s.samples) return null;
231
+ if (!s.buffer) {
232
+ s.buffer = ctx.createBuffer(1, s.samples.length, 48000);
233
+ s.buffer.copyToChannel(s.samples, 0);
234
+ }
235
+ return s.buffer;
236
+ }
237
+
238
+ export const sound = {
239
+ // An effect made in code: a wave ('sine', 'square', 'triangle', 'saw', 'noise') from one pitch to another under a
240
+ // simple envelope, in seconds.
241
+ tone({ wave = 'square', from = 440, to = from, attack = 0.005, hold = 0.08, release = 0.1, volume = 0.6, name } = {}) {
242
+ const n = Math.max(1, Math.round((attack + hold + release) * 48000));
243
+ const out = new Float32Array(n);
244
+ let phase = 0, noise = 0x2545F491;
245
+ for (let i = 0; i < n; i++) {
246
+ const t = i / 48000, at = t / Math.max(1e-6, attack + hold + release);
247
+ const hz = from + (to - from) * at;
248
+ phase = (phase + hz / 48000) % 1;
249
+ let v;
250
+ if (wave === 'sine') v = Math.sin(phase * 2 * Math.PI);
251
+ else if (wave === 'triangle') v = 1 - 4 * Math.abs(phase - 0.5);
252
+ else if (wave === 'saw') v = 2 * phase - 1;
253
+ else if (wave === 'noise') { noise ^= noise << 13; noise ^= noise >>> 17; noise ^= noise << 5; v = ((noise >>> 0) / 4294967296) * 2 - 1; }
254
+ else v = phase < 0.5 ? 1 : -1;
255
+ const env = t < attack ? t / attack : t < attack + hold ? 1 : Math.max(0, 1 - (t - attack - hold) / Math.max(1e-6, release));
256
+ out[i] = v * env * volume * 0.5;
257
+ }
258
+ return made(out, name || `${wave} ${Math.round(from)}→${Math.round(to)} Hz`);
259
+ },
260
+ // An effect from samples worked out in code: mono, -1 to 1, at `rate` a second.
261
+ samples(values, rate = 48000, name = 'samples') {
262
+ const n = Math.round(values.length * 48000 / rate), out = new Float32Array(n);
263
+ for (let i = 0; i < n; i++) out[i] = values[Math.min(values.length - 1, Math.floor(i * rate / 48000))] || 0;
264
+ return made(out, name);
265
+ },
266
+ // A sound file beside the page (.wav, .ogg, .mp3, .m4a), decoded whole: load each in init, never per frame.
267
+ async load(path) {
268
+ const s = made(null, path);
269
+ // A new copy of the file is decoded into the same sound, so what plays it next plays the new one.
270
+ const decode = async () => {
271
+ const ctx = context();
272
+ if (!ctx) return;
273
+ try { s.buffer = await ctx.decodeAudioData(await (await fetch(fresh(path))).arrayBuffer()); } catch (e) { log(`sound: couldn't load ${path}`); }
274
+ };
275
+ took(clean(path));
276
+ swaps.set(clean(path), [...(swaps.get(clean(path)) || []), decode]);
277
+ await decode();
278
+ return s;
279
+ },
280
+ // Plays an effect, and gives the voice playing it (null when it didn't): volume 0 to 1, pan -1 to 1, pitch 1 as
281
+ // made (2 an octave up). loop plays it over and over until stopped.
282
+ play(s, { volume = 1, pan = 0, pitch = 1, loop = false } = {}) {
283
+ if (!s) return null;
284
+ const ctx = context(), buffer = bufferOf(s) || s.buffer;
285
+ if (!ctx || !buffer) return null;
286
+ const source = ctx.createBufferSource(), gain = ctx.createGain(), panner = ctx.createStereoPanner();
287
+ source.buffer = buffer; source.loop = loop; source.playbackRate.value = pitch;
288
+ gain.gain.value = volume * effectsVolume; panner.pan.value = pan;
289
+ source.connect(gain).connect(panner).connect(ctx.destination);
290
+ source.start();
291
+ const voice = { source, gain, panner, volume };
292
+ voices.add(voice);
293
+ source.onended = () => voices.delete(voice);
294
+ return voice;
295
+ },
296
+ set(voice, { volume, pan, pitch } = {}) {
297
+ if (!voice) return;
298
+ if (volume != null) { voice.volume = volume; voice.gain.gain.value = volume * effectsVolume; }
299
+ if (pan != null) voice.panner.pan.value = pan;
300
+ if (pitch != null) voice.source.playbackRate.value = pitch;
301
+ },
302
+ stop(voice) { try { voice && voice.source.stop(); } catch (e) {} },
303
+ // Music: a sound played over and over under the effects (keep it quiet, 0.3 to 0.5); null fades it out.
304
+ music(s, volume = 0.4) {
305
+ const ctx = context();
306
+ if (musicVoice && ctx) { const old = musicVoice; old.gain.gain.setTargetAtTime(0, ctx.currentTime, 0.3); after(1500, () => sound.stop(old)); }
307
+ musicVoice = null;
308
+ music.gain = volume;
309
+ if (!s || !ctx) return;
310
+ const buffer = bufferOf(s) || s.buffer;
311
+ if (!buffer) return;
312
+ const source = ctx.createBufferSource(), gain = ctx.createGain(), panner = ctx.createStereoPanner();
313
+ source.buffer = buffer; source.loop = true;
314
+ gain.gain.value = 0;
315
+ gain.gain.setTargetAtTime(volume * musicVolume, ctx.currentTime, 0.3);
316
+ source.connect(gain).connect(panner).connect(ctx.destination);
317
+ source.start();
318
+ musicVoice = { source, gain, panner };
319
+ },
320
+ // The person's own volumes for effects and for music, 0 to 1: what the game's settings change, kept with save().
321
+ volume(effects, musicLevel) {
322
+ effectsVolume = effects; musicVolume = musicLevel;
323
+ for (const voice of voices) voice.gain.gain.value = voice.volume * effectsVolume;
324
+ if (musicVoice) musicVoice.gain.gain.value = music.gain * musicVolume;
325
+ },
326
+ };
327
+
328
+ // MARK: The controllers
329
+
330
+ const keys = new Set();
331
+ // Only the keyboard's own keys: the Xbox's lamp.js sends the controller as key events too (which aren't trusted, being
332
+ // made by a script), and the controller is read whole through the Gamepad API.
333
+ // A Samsung or LG TV's remote names its keys only by number: its arrows are the keyboard's, OK is Enter, Back is Escape.
334
+ const REMOTE = { 37: 'ArrowLeft', 38: 'ArrowUp', 39: 'ArrowRight', 40: 'ArrowDown', 13: 'Enter', 10009: 'Escape', 461: 'Escape' };
335
+ const codeOf = e => e.code || REMOTE[e.keyCode] || '';
336
+ window.addEventListener('keydown', e => { if (!e.isTrusted) return; const code = codeOf(e); keys.add(code); if (code.startsWith('Arrow') || code === 'Space' || code === 'Tab') e.preventDefault(); });
337
+ window.addEventListener('keyup', e => { if (e.isTrusted) keys.delete(codeOf(e)); });
338
+ window.addEventListener('blur', () => keys.clear());
339
+
340
+ // The keyboard as a pad: the keys for each button and for each way a stick or trigger goes, the first the one a
341
+ // prompt names (`label`), but for the left stick, where a game's `keyboardHints` can name the arrows instead.
342
+ const KEYS = { [BUTTON.A]: ['Space', 'Enter'], [BUTTON.B]: ['Escape', 'Backspace'], [BUTTON.X]: ['KeyF'], [BUTTON.Y]: ['KeyR'],
343
+ [BUTTON.LB]: ['KeyQ'], [BUTTON.RB]: ['KeyE'], [BUTTON.MENU]: ['KeyP'], [BUTTON.VIEW]: ['Tab'],
344
+ [BUTTON.UP]: ['ArrowUp'], [BUTTON.DOWN]: ['ArrowDown'], [BUTTON.LEFT]: ['ArrowLeft'], [BUTTON.RIGHT]: ['ArrowRight'] };
345
+ const STICK_KEYS = { left: { up: ['KeyW', 'ArrowUp'], left: ['KeyA', 'ArrowLeft'], down: ['KeyS', 'ArrowDown'], right: ['KeyD', 'ArrowRight'] },
346
+ right: { up: ['KeyI'], left: ['KeyJ'], down: ['KeyK'], right: ['KeyL'] } };
347
+ const TRIGGER_KEYS = { lt: ['KeyZ'], rt: ['KeyC'] };
348
+ // Which of the left stick's keys a prompt names, by their place in STICK_KEYS.left. Both always move the stick.
349
+ const LEFT_STICK_HINTS = { wasd: 0, arrows: 1 };
350
+ let leftStickHint = LEFT_STICK_HINTS.wasd;
351
+
352
+ function keyboardPad() {
353
+ const any = codes => codes.some(c => keys.has(c)), axis = (plus, minus) => (any(plus) ? 1 : 0) - (any(minus) ? 1 : 0);
354
+ const l = STICK_KEYS.left, r = STICK_KEYS.right;
355
+ const pad = { lx: axis(l.right, l.left), ly: axis(l.up, l.down), rx: axis(r.right, r.left), ry: axis(r.up, r.down),
356
+ lt: any(TRIGGER_KEYS.lt) ? 1 : 0, rt: any(TRIGGER_KEYS.rt) ? 1 : 0, down: 0 };
357
+ for (const bit in KEYS) if (any(KEYS[bit])) pad.down |= Number(bit);
358
+ return pad;
359
+ }
360
+
361
+ const dead = v => Math.abs(v) < 0.15 ? 0 : (v - Math.sign(v) * 0.15) / 0.85;
362
+ function gamepadPad(g) {
363
+ const b = i => g.buttons[i] && g.buttons[i].pressed;
364
+ const pad = { connected: true, lx: dead(g.axes[0] || 0), ly: -dead(g.axes[1] || 0), rx: dead(g.axes[2] || 0), ry: -dead(g.axes[3] || 0),
365
+ lt: g.buttons[6] ? g.buttons[6].value : 0, rt: g.buttons[7] ? g.buttons[7].value : 0, down: 0 };
366
+ const bits = [BUTTON.A, BUTTON.B, BUTTON.X, BUTTON.Y, BUTTON.LB, BUTTON.RB, 0, 0, BUTTON.VIEW, BUTTON.MENU, BUTTON.LSTICK, BUTTON.RSTICK,
367
+ BUTTON.UP, BUTTON.DOWN, BUTTON.LEFT, BUTTON.RIGHT];
368
+ bits.forEach((bit, i) => { if (bit && b(i)) pad.down |= bit; });
369
+ pad.gamepad = g;
370
+ return pad;
371
+ }
372
+
373
+ // The first controller as an app around the page reads it itself (Lamp's Android app, whose WebView doesn't promise
374
+ // the Gamepad API): sticks -1 to 1 with up positive, triggers 0 to 1, buttons as BUTTON bits.
375
+ // `vendor` is the controller's USB vendor, which an app from before it was sent leaves out.
376
+ let nativePad = null;
377
+ // All at rest is no controller in hand (one that went away sends that), so the touch controls can come back.
378
+ window.lampNativePad = (lx, ly, rx, ry, lt, rt, down, vendor) => {
379
+ nativePad = down || lx || ly || rx || ry || lt || rt ? { connected: true, lx, ly, rx, ry, lt, rt, down, vendor } : null;
380
+ };
381
+
382
+ function merge(pad, more) {
383
+ pad.down |= more.down;
384
+ for (const axis of ['lx', 'ly', 'rx', 'ry', 'lt', 'rt']) if (Math.abs(more[axis]) > Math.abs(pad[axis])) pad[axis] = more[axis];
385
+ }
386
+
387
+ // The controller's rumble, 0 to 1 for its two motors, for `seconds` (a short shake by default).
388
+ export function rumble(pad, low, high, seconds = 0.2) {
389
+ const g = (pads[pad] || {}).gamepad;
390
+ g && g.vibrationActuator && g.vibrationActuator.playEffect('dual-rumble', { duration: seconds * 1000, strongMagnitude: low, weakMagnitude: high }).catch(() => {});
391
+ }
392
+
393
+ // MARK: Touch controls, on a touch screen while no controller is connected, or once the screen is touched
394
+
395
+ let touchButtons = BUTTON.A | BUTTON.B | BUTTON.MENU;
396
+ // A page with touch controls of its own, or one that reads the keyboard itself, turns Lamp's off (`game`).
397
+ let drawsTouch = true, readsKeyboard = true;
398
+ const touch = { lx: 0, ly: 0, rx: 0, ry: 0, lt: 0, rt: 0, down: 0 };
399
+ // A tap shorter than a frame still reaches the game: held for the next frame it reads.
400
+ let tapped = 0;
401
+ let touchShown = false, touchLayer = null;
402
+ const touchScreen = ('ontouchstart' in window || navigator.maxTouchPoints > 0);
403
+
404
+ function buildTouch() {
405
+ touchLayer = document.createElement('div');
406
+ touchLayer.style.cssText = 'position:fixed;inset:0;z-index:10;touch-action:none;user-select:none;-webkit-user-select:none;font:700 22px -apple-system,system-ui,sans-serif;color:#fff';
407
+ const stick = (side, axes) => {
408
+ const zone = document.createElement('div');
409
+ zone.style.cssText = `position:absolute;top:0;bottom:0;${side}:0;width:50%`;
410
+ const ring = document.createElement('div'), knob = document.createElement('div');
411
+ ring.style.cssText = 'position:absolute;width:120px;height:120px;margin:-60px 0 0 -60px;border-radius:50%;background:rgba(255,255,255,.18);display:none';
412
+ knob.style.cssText = 'position:absolute;width:54px;height:54px;margin:-27px 0 0 -27px;border-radius:50%;background:rgba(255,255,255,.45);display:none';
413
+ zone.append(ring, knob);
414
+ // Where the left stick is, before a thumb is on it.
415
+ const rest = document.createElement('div');
416
+ if (side === 'left') {
417
+ rest.style.cssText = 'position:absolute;left:calc(40px + env(safe-area-inset-left));bottom:calc(32px + env(safe-area-inset-bottom));width:120px;height:120px;border-radius:50%;border:2px solid rgba(255,255,255,.25)';
418
+ zone.append(rest);
419
+ }
420
+ let id = null, sx = 0, sy = 0;
421
+ listen(zone, 'pointerdown', e => { id = e.pointerId; sx = e.clientX; sy = e.clientY; zone.setPointerCapture(id); move(e); });
422
+ const move = e => {
423
+ if (e.pointerId !== id) return;
424
+ let dx = e.clientX - sx, dy = e.clientY - sy;
425
+ const length = Math.hypot(dx, dy), scale = Math.min(1, 60 / Math.max(1, length));
426
+ dx *= scale; dy *= scale;
427
+ touch[axes[0]] = dx / 60; touch[axes[1]] = -dy / 60;
428
+ const r = zone.getBoundingClientRect();
429
+ ring.style.left = (sx - r.left) + 'px'; ring.style.top = (sy - r.top) + 'px';
430
+ knob.style.left = (sx - r.left + dx) + 'px'; knob.style.top = (sy - r.top + dy) + 'px';
431
+ ring.style.display = knob.style.display = 'block';
432
+ rest.style.display = 'none';
433
+ };
434
+ listen(zone, 'pointermove', move);
435
+ const end = e => {
436
+ if (e.pointerId !== id) return;
437
+ id = null; touch[axes[0]] = touch[axes[1]] = 0;
438
+ ring.style.display = knob.style.display = 'none';
439
+ rest.style.display = '';
440
+ };
441
+ listen(zone, 'pointerup', end); listen(zone, 'pointercancel', end);
442
+ return zone;
443
+ };
444
+ touchLayer.append(stick('left', ['lx', 'ly']));
445
+ if (touchButtons & BUTTON.RSTICK) touchLayer.append(stick('right', ['rx', 'ry']));
446
+ const button = (bit, title, css, small) => {
447
+ if (!(touchButtons & bit)) return;
448
+ const size = small ? 44 : 64;
449
+ const b = document.createElement('div');
450
+ b.textContent = title;
451
+ b.style.cssText = `position:absolute;${css};width:${size}px;height:${size}px;line-height:${size}px;text-align:center;border-radius:50%;background:rgba(255,255,255,.22);font-size:${small ? 15 : 22}px`;
452
+ const on = e => { e.stopPropagation(); touch.down |= bit; tapped |= bit; b.style.background = 'rgba(255,255,255,.5)'; b.setPointerCapture(e.pointerId); };
453
+ const off = () => { touch.down &= ~bit; b.style.background = 'rgba(255,255,255,.22)'; };
454
+ listen(b, 'pointerdown', on); listen(b, 'pointerup', off); listen(b, 'pointercancel', off);
455
+ touchLayer.append(b);
456
+ };
457
+ // Kept clear of a phone's rounded corners and its camera (the safe area).
458
+ const at = (x, side, y, edge) => `${side}:calc(${x}px + env(safe-area-inset-${side}));${edge}:calc(${y}px + env(safe-area-inset-${edge}))`;
459
+ const face = bit => TOUCH_FACES[bit] || BASE.names[nameOf(bit)];
460
+ button(BUTTON.A, face(BUTTON.A), at(110, 'right', 40, 'bottom'));
461
+ button(BUTTON.B, face(BUTTON.B), at(40, 'right', 110, 'bottom'));
462
+ button(BUTTON.X, face(BUTTON.X), at(180, 'right', 110, 'bottom'));
463
+ button(BUTTON.Y, face(BUTTON.Y), at(110, 'right', 180, 'bottom'));
464
+ button(BUTTON.LB, face(BUTTON.LB), at(30, 'left', 20, 'top'), true);
465
+ button(BUTTON.VIEW, face(BUTTON.VIEW), at(90, 'left', 20, 'top'), true);
466
+ button(BUTTON.MENU, face(BUTTON.MENU), at(90, 'right', 20, 'top'), true);
467
+ button(BUTTON.RB, face(BUTTON.RB), at(30, 'right', 20, 'top'), true);
468
+ document.body.append(touchLayer);
469
+ }
470
+ if (touchScreen) window.addEventListener('pointerdown', () => { if (!touchShown) { touchShown = true; showTouch(); } }, { capture: true });
471
+ function showTouch() { if (!drawsTouch) return; if (!touchLayer) buildTouch(); touchLayer.style.display = 'block'; touchShown = true; }
472
+ function hideTouch() { if (touchLayer) touchLayer.style.display = 'none'; touchShown = false; }
473
+
474
+ // MARK: What the player plays with, and what to call its buttons
475
+
476
+ // The page never works out which device it's on. Lamp writes it beside this file in every copy of the page
477
+ // (lamp-device.js, from the device's row in Lamp's table of devices): what its keyboard calls the keys keyboards name
478
+ // differently, the controller that comes with it, and the one layout every controller there has, where the device
479
+ // has one. The controllers a game names buttons for, and how each is told apart, are Lamp's list (lamp-controllers.js).
480
+ const LAYOUTS = Object.fromEntries(controllers.layouts.map(layout => [layout.id, layout]));
481
+ const BASE = LAYOUTS[controllers.base];
482
+
483
+ // The input the player used last: a controller (and whose), the keyboard (with the mouse) or the touch screen. A
484
+ // device that comes with a controller starts on it, a touch screen on touch, anything else on the
485
+ // keyboard (the controller named by the device's one layout where it has one, as every controller there is); it
486
+ // changes as soon as the player presses a key, touches the screen or moves a controller.
487
+ let current = device.controller ? { kind: 'controller', family: device.layout || device.controller }
488
+ : touchScreen ? { kind: 'touch' } : { kind: 'keyboard' };
489
+ // The controller's own name, for a model that marks a button differently from the rest of its layout (a DualShock's
490
+ // Share where a DualSense has Create).
491
+ let padName = '';
492
+ const variantOf = (family, name) => ((LAYOUTS[family] || {}).variants || []).find(v => new RegExp(v.match, 'i').test(name)) || null;
493
+ const listeners = new Set();
494
+
495
+ export function input() { return Object.assign({}, current); }
496
+ // Calls `changed(input())` now and again each time the player takes up another input, to draw the prompts and help
497
+ // for it; returns a function that stops it.
498
+ export function onInput(changed) {
499
+ listeners.add(changed);
500
+ try { changed(input()); } catch (e) { log(`Error: ${e.message}`); }
501
+ return () => listeners.delete(changed);
502
+ }
503
+
504
+ const drawPrompts = () => { for (const changed of listeners) try { changed(input()); } catch (e) { log(`Error: ${e.message}`); } };
505
+
506
+ function use(kind, family, name = '') {
507
+ // A new controller of the same family can still change a label (a DualShock's Share where a DualSense has Create).
508
+ const relabels = variantOf(family, name) !== variantOf(family, padName);
509
+ padName = name;
510
+ if (current.kind === kind && current.family === family && !relabels) return;
511
+ current = family ? { kind, family } : { kind };
512
+ log(`Lamp: playing with ${family ? (LAYOUTS[family] || {}).spoken || controllers.unknown : 'the ' + kind}.`);
513
+ drawPrompts();
514
+ }
515
+
516
+ // Whose controller it is: the device's one layout where it has one, or else what the controller reports, its name as
517
+ // the browser gives it (with its USB vendor in Chrome's) or the vendor an app around the page sends with the pad. One
518
+ // from an app that doesn't send a vendor is taken as the base layout.
519
+ function familyOf(id) {
520
+ if (device.layout) return device.layout;
521
+ const found = controllers.layouts.find(layout => new RegExp(layout.match, 'i').test(id));
522
+ return found ? found.id : 'other';
523
+ }
524
+ function vendorFamily(vendor) {
525
+ if (device.layout) return device.layout;
526
+ if (!vendor) return controllers.base;
527
+ const found = controllers.layouts.find(layout => layout.vendors.includes(vendor));
528
+ return found ? found.id : 'other';
529
+ }
530
+
531
+ window.addEventListener('keydown', e => { if (e.isTrusted) use('keyboard'); }, { capture: true });
532
+ window.addEventListener('pointerdown', e => {
533
+ if (e.pointerType === 'touch' || e.pointerType === 'pen') use('touch'); else if (e.pointerType === 'mouse') use('keyboard');
534
+ }, { capture: true, passive: true });
535
+
536
+ // What a prompt names each part of the pad by is the layout's (a part it leaves out is the base layout's): BUTTON.A
537
+ // is a place, the bottom face button, so a prompt names what's printed on the button in that place on the player's
538
+ // controller. The touch controls name their word buttons as these do.
539
+ const TOUCH_NAMES = { MENU: 'Menu', VIEW: 'View' };
540
+ // What the touch controls draw on a button whose name is a word.
541
+ const TOUCH_FACES = { [BUTTON.MENU]: '≡', [BUTTON.VIEW]: '⧉' };
542
+ const nameOf = what => typeof what === 'number' ? Object.keys(BUTTON).find(name => BUTTON[name] === what)
543
+ : ({ [STICK.LEFT]: 'STICK_L', [STICK.RIGHT]: 'STICK_R', [TRIGGER.LT]: 'LT', [TRIGGER.RT]: 'RT' })[what];
544
+
545
+ // A key's name as it's printed on the key, from its code (KeyboardEvent.code): the device's own name for it, or a PC
546
+ // keyboard's.
547
+ function keyName(code) {
548
+ const named = { Space: 'Space', Enter: 'Enter', Escape: 'Esc', Backspace: 'Backspace', Tab: 'Tab',
549
+ ArrowUp: '↑', ArrowDown: '↓', ArrowLeft: '←', ArrowRight: '→', ShiftLeft: 'Shift', ShiftRight: 'Shift',
550
+ ControlLeft: 'Ctrl', ControlRight: 'Ctrl', AltLeft: 'Alt', AltRight: 'Alt',
551
+ MetaLeft: 'Win', MetaRight: 'Win', Minus: '-', Equal: '=', Comma: ',', Period: '.', Slash: '/' };
552
+ return (device.keys || {})[code] || named[code] || (/^(Key|Digit|Numpad)(.+)$/.exec(code) || [])[2] || code;
553
+ }
554
+
555
+ function keysFor(name) {
556
+ if (name === 'STICK_L' || name === 'STICK_R') {
557
+ const s = STICK_KEYS[name === 'STICK_L' ? 'left' : 'right'], at = name === 'STICK_L' ? leftStickHint : 0;
558
+ return [s.up[at], s.left[at], s.down[at], s.right[at]];
559
+ }
560
+ if (name === 'LT' || name === 'RT') return [TRIGGER_KEYS[name.toLowerCase()][0]];
561
+ const codes = KEYS[BUTTON[name]];
562
+ return codes ? [codes[0]] : null;
563
+ }
564
+
565
+ // What to call a button (BUTTON.A…), a stick (STICK.LEFT) or a trigger (TRIGGER.RT) on the input the player is using
566
+ // (or the one given): "A", "✕", "Space", "W A S D" ("↑ ← ↓ →" with game()'s `keyboardHints`). A key's code ('KeyH'), for a page that reads keys itself, is that
567
+ // key's name whatever the input. null when the input has nothing for it (no key for a stick's click, no touch button
568
+ // the game didn't ask for), so the prompt is left out.
569
+ export function label(what, on = current) {
570
+ const name = nameOf(what);
571
+ if (!name) return typeof what === 'string' ? keyName(what) : null;
572
+ if (on.kind === 'keyboard') { const codes = readsKeyboard ? keysFor(name) : null; return codes ? codes.map(keyName).join(' ') : null; }
573
+ if (on.kind === 'touch') {
574
+ if (!drawsTouch) return null;
575
+ if (name === 'STICK_L') return BASE.names.STICK_L;
576
+ if (name === 'STICK_R') return touchButtons & BUTTON.RSTICK ? BASE.names.STICK_R : null;
577
+ const bit = BUTTON[name];
578
+ return bit && bit < BUTTON.UP && (touchButtons & bit) ? TOUCH_NAMES[name] || BASE.names[name] : null;
579
+ }
580
+ const variant = variantOf(on.family, padName);
581
+ return (variant && variant.names[name]) || ((LAYOUTS[on.family] || {}).names || {})[name] || BASE.names[name];
582
+ }
583
+
584
+ // A badge for a button, stick or trigger as the player's input draws it (an Xbox controller's colored round letters,
585
+ // a PlayStation controller's shapes, a key's keycap), sized to the text around it: a <span> to put in a prompt or
586
+ // a help screen, or null where `label` is.
587
+ export function glyph(what, on = current) {
588
+ const text = label(what, on);
589
+ if (text == null) return null;
590
+ const name = nameOf(what), el = document.createElement('span');
591
+ el.setAttribute('role', 'img');
592
+ el.setAttribute('aria-label', text);
593
+ el.style.cssText = 'display:inline-flex;gap:.18em;vertical-align:middle;line-height:1;white-space:nowrap';
594
+ const badge = (content, css) => {
595
+ const b = document.createElement('span');
596
+ b.textContent = content;
597
+ b.style.cssText = 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;height:1.7em;min-width:1.7em;'
598
+ + 'padding:0 .4em;font:700 .8em/1 -apple-system,system-ui,"Segoe UI",sans-serif;letter-spacing:0;' + css;
599
+ el.append(b);
600
+ };
601
+ if (on.kind === 'keyboard' || !name) {
602
+ // A key: a light keycap with its lower edge, one for each key a stick takes.
603
+ for (const key of text.split(' ')) badge(key, 'border-radius:.35em;background:linear-gradient(#fdfdfe,#e3e4e8);color:#1d1d22;'
604
+ + 'border:1px solid #a9abb3;border-bottom-width:.2em;box-shadow:0 1px 2px rgba(0,0,0,.35)' + (key.length > 1 ? ';padding:0 .55em' : ''));
605
+ return el;
606
+ }
607
+ const dark = 'background:#18191d;color:#fff;border:.12em solid rgba(255,255,255,.28);box-shadow:0 1px 3px rgba(0,0,0,.45);';
608
+ if (on.kind === 'touch') {
609
+ const face = TOUCH_FACES[BUTTON[name]] || text;
610
+ badge(name.startsWith('STICK') ? '' : face, 'border-radius:50%;background:rgba(255,255,255,.22);color:#fff;'
611
+ + (name.startsWith('STICK') ? 'border:.12em solid rgba(255,255,255,.45)' : 'border:0'));
612
+ return el;
613
+ }
614
+ const layout = LAYOUTS[on.family] || {};
615
+ if (['A', 'B', 'X', 'Y'].includes(name)) {
616
+ const colour = (layout.faces || {})[name] || '#fff';
617
+ badge(text, dark + `border-radius:50%;padding:0;color:${colour};` + (layout.faceSize ? `font-size:${layout.faceSize}` : ''));
618
+ } else if (name.startsWith('STICK')) {
619
+ // A stick: a ring with its side's letter in it.
620
+ badge(name === 'STICK_L' ? 'L' : 'R', dark + 'border-radius:50%;padding:0;box-shadow:inset 0 0 0 .2em #18191d,inset 0 0 0 .3em rgba(255,255,255,.35)');
621
+ } else if (['UP', 'DOWN', 'LEFT', 'RIGHT'].includes(name)) {
622
+ badge(({ UP: '↑', DOWN: '↓', LEFT: '←', RIGHT: '→' })[name], dark + 'border-radius:.3em;padding:0');
623
+ } else if (name === 'LT' || name === 'RT') {
624
+ badge(text, dark + 'border-radius:.75em .75em .3em .3em');
625
+ } else if (name === 'LB' || name === 'RB') {
626
+ badge(text, dark + 'border-radius:.3em .3em .6em .6em');
627
+ } else if ((layout.menuGlyphs || {})[name]) {
628
+ badge(layout.menuGlyphs[name], dark + 'border-radius:50%;padding:0');
629
+ } else {
630
+ // Menu and View by their words (Options, Create, + and −), and a stick's click.
631
+ badge(text, dark + 'border-radius:.85em;' + (text.length > 2 ? 'font-size:.62em;text-transform:uppercase;letter-spacing:.04em' : ''));
632
+ }
633
+ return el;
634
+ }
635
+
636
+ // MARK: A fixed step, for physics
637
+
638
+ // Physics (Rapier's world.step(), or the game's own) wants the same step every time, whatever the device's frame rate:
639
+ // a step that follows the frame bounces and tunnels differently at 30, 60 and 120 frames a second. fixedStep(step) gives a function to call once a frame with f.dt: it
640
+ // runs step() as many times as the time since the last frame holds, at 1/60 s each (`rate` a second), and at most `most`
641
+ // times, so a device that can't keep up slows the game down rather than falling further behind. It returns how far the
642
+ // frame is into the next step, 0 to 1, for a game that places what it draws between the last two steps (most needn't).
643
+ //
644
+ // const physics = fixedStep(() => world.step(events));
645
+ // frame(f) { steer(f.pads[0]); physics(f.dt); place(); renderer.render(scene, camera); }
646
+ export function fixedStep(step, { rate = 60, most = 4 } = {}) {
647
+ const h = 1 / rate;
648
+ let owed = 0;
649
+ return dt => {
650
+ owed += dt;
651
+ let n = 0;
652
+ // A hair under a whole step still counts as one, so a frame of 1/60 s is always one step of 1/60 s.
653
+ while (owed >= h - 1e-9 && n < most) { step(h); owed -= h; n++; }
654
+ if (n === most) owed = Math.min(owed, h);
655
+ return Math.max(0, owed) / h;
656
+ };
657
+ }
658
+
659
+ // MARK: The frame
660
+
661
+ let time = 0, number = 0;
662
+ const pads = [0, 1, 2, 3].map(() => ({ connected: false, lx: 0, ly: 0, rx: 0, ry: 0, lt: 0, rt: 0, down: 0, pressed: 0, released: 0 }));
663
+ let renderer = null, frameFunction = null, before = [0, 0, 0, 0], seenControllers = -1;
664
+
665
+ // Whether a controller is being played with now: a button down or a stick off its center.
666
+ const inUse = g => g.buttons.some(b => b && b.pressed) || g.axes.some(a => Math.abs(a) > 0.3);
667
+
668
+ function readPads() {
669
+ if (driven && clockNow() > drivenUntil) driven = null;
670
+ if (lampHas && clockNow() > lampHasUntil) letGoOfLamp(null);
671
+ const found = Array.from(navigator.getGamepads ? navigator.getGamepads() : []).filter(g => g && g.connected);
672
+ // The touch controls are there from the start, step aside once a controller is played with (a connected one alone
673
+ // may only be the simulator passing the Mac's through), and come back when the screen is touched.
674
+ const played = found.find(inUse);
675
+ const nativeInUse = nativePad && (nativePad.down || [nativePad.lx, nativePad.ly, nativePad.rx, nativePad.ry, nativePad.lt, nativePad.rt].some(a => Math.abs(a) > 0.3));
676
+ if (touchScreen) {
677
+ if (seenControllers === -1) showTouch();
678
+ if (touchShown && (played || nativeInUse)) hideTouch();
679
+ }
680
+ // The controller played with is the input now.
681
+ if (played) use('controller', familyOf(played.id), played.id);
682
+ else if (nativeInUse) use('controller', vendorFamily(nativePad.vendor));
683
+ if (found.length !== seenControllers) { seenControllers = found.length; log(`Lamp: ${found.length} controller${found.length === 1 ? '' : 's'} connected.`); }
684
+ for (let i = 0; i < 4; i++) {
685
+ let pad = found[i] ? gamepadPad(found[i]) : { connected: false, lx: 0, ly: 0, rx: 0, ry: 0, lt: 0, rt: 0, down: 0 };
686
+ if (i === 0) {
687
+ if (nativePad) merge(pad, nativePad);
688
+ if (readsKeyboard) merge(pad, keyboardPad());
689
+ merge(pad, Object.assign({}, touch, { down: touch.down | tapped }));
690
+ tapped = 0;
691
+ // The person's own input while Lamp has the controller, holding something or not, takes the game back.
692
+ if (lampHas && (played || nativeInUse || pressing(pad))) letGoOfLamp('the person played');
693
+ if (driven) pad = Object.assign({}, driven);
694
+ pad.connected = true;
695
+ }
696
+ pad.pressed = pad.down & ~before[i];
697
+ pad.released = before[i] & ~pad.down;
698
+ before[i] = pad.down;
699
+ pads[i] = pad;
700
+ }
701
+ }
702
+
703
+ // Phones' buttons, as a frame reads them. Lamp.controllers hears a button between frames, so what a game does at the top
704
+ // of a frame can't lose it: what each button did is kept here until the next frame takes it, a press and release between
705
+ // two frames still counting as both. `onButton` goes on as it was, for what can't wait for a frame. Held buttons follow
706
+ // the phone at all times. Presses and releases not yet taken go with a phone that leaves, and with the run of the game's
707
+ // code that was playing: what arrives while the code runs again changes what's held and is never replayed as a press.
708
+ const phoneButtons = new Map();
709
+ function phoneButton(player, name, down) {
710
+ if (down && lampHas) letGoOfLamp('a phone played');
711
+ let buttons = phoneButtons.get(player);
712
+ if (!buttons) phoneButtons.set(player, buttons = newPhoneButtons());
713
+ if (!!buttons.down[name] === down) return;
714
+ if (down) buttons.down[name] = true; else delete buttons.down[name];
715
+ if (!phonesWaiting) (down ? buttons.pressed : buttons.released)[name] = true;
716
+ }
717
+ // A button's name comes from the phone, so nothing here inherits one.
718
+ const newPhoneButtons = () => ({ down: Object.create(null), pressed: Object.create(null), released: Object.create(null) });
719
+ function forgetPhoneEdges() {
720
+ for (const buttons of phoneButtons.values()) { buttons.pressed = Object.create(null); buttons.released = Object.create(null); }
721
+ }
722
+ function readPhones() {
723
+ if (!window.Lamp || !window.Lamp.controllers) return [];
724
+ const list = window.Lamp.controllers.players().map(player => {
725
+ const buttons = phoneButtons.get(player) || newPhoneButtons();
726
+ return { player, down: Object.assign(Object.create(null), buttons.down), pressed: buttons.pressed, released: buttons.released };
727
+ });
728
+ forgetPhoneEdges();
729
+ return list;
730
+ }
731
+
732
+ function step(dt) {
733
+ readPads();
734
+ const phonesNow = readPhones();
735
+ time += dt;
736
+ const said = frameFunction({ dt, time, number, width: WIDTH, height: HEIGHT, pads, phones: phonesNow });
737
+ number++;
738
+ return said;
739
+ }
740
+
741
+ // MARK: The frame rate, measured on the device
742
+
743
+ // Every ten seconds of play the log says how fast frames really came, so the agent sees the device's own frame rate.
744
+ // And when frames run slow the game is drawn smaller and scaled up to the screen, a
745
+ // step at a time: on whatever device it is, a scene that fills the screen with pixels is the usual cost, and fewer
746
+ // pixels the usual cure. A step that doesn't make frames faster is undone and no more are tried, since then the
747
+ // drawing isn't what's slow (a busy frame function, or a screen that refreshes only 30 times a second).
748
+ const SCALES = [1, 0.75, 0.5], SLOW = 45;
749
+ let level = 0, adapting = true, rateBefore = 0, lastTick = 0;
750
+ const report = { frames: 0, time: 0, slowest: 0 }, check = { times: [], time: 0, warm: false };
751
+
752
+ function drawScale(to) {
753
+ level = to;
754
+ check.warm = false;
755
+ renderer.setSize(Math.round(WIDTH * SCALES[level]), Math.round(HEIGHT * SCALES[level]), false);
756
+ }
757
+ // A page that sizes its own canvas (`fill`) is drawn at whatever it chose.
758
+ let fills = false;
759
+ const drawn = () => fills ? `${renderer.domElement.width}x${renderer.domElement.height}`
760
+ : `${Math.round(WIDTH * SCALES[level])}x${Math.round(HEIGHT * SCALES[level])}`;
761
+
762
+ function measure(now) {
763
+ const ms = lastTick ? now - lastTick : 0;
764
+ lastTick = now;
765
+ // A page out of sight gets no frames: the gap is a pause, not a slow frame.
766
+ if (!ms || ms > 1000) return;
767
+ report.frames++; report.time += ms; report.slowest = Math.max(report.slowest, ms);
768
+ if (report.time >= 10000) {
769
+ log(`Lamp: ${Math.round(report.frames * 1000 / report.time)} frames a second (frames ${(report.time / report.frames).toFixed(1)} ms on average, slowest ${report.slowest.toFixed(1)} ms), drawn at ${drawn()}.`);
770
+ report.frames = report.time = report.slowest = 0;
771
+ }
772
+ if (!adapting) return;
773
+ check.times.push(ms); check.time += ms;
774
+ // Two seconds a check, after a second left out as play starts and after each change of size, for the shaders
775
+ // compiled, files read and buffers made then. The rate is the typical frame's, the median: one long hitch (a
776
+ // window shown, a file read) isn't a device too slow to draw.
777
+ if (check.time < (check.warm ? 2000 : 1000)) return;
778
+ const sorted = check.times.sort((a, b) => a - b), rate = 1000 / sorted[sorted.length >> 1], warm = check.warm;
779
+ check.times = []; check.time = 0; check.warm = true;
780
+ if (!warm) return;
781
+ if (rateBefore) {
782
+ if (rate < rateBefore * 1.15) {
783
+ drawScale(level - 1);
784
+ adapting = false;
785
+ log(`Lamp: drawing smaller didn't make frames faster (${Math.round(rateBefore)} a second, then ${Math.round(rate)}), so the game is drawn at ${drawn()} again.`);
786
+ return;
787
+ }
788
+ rateBefore = 0;
789
+ }
790
+ if (rate < SLOW && level < SCALES.length - 1) {
791
+ rateBefore = rate;
792
+ drawScale(level + 1);
793
+ log(`Lamp: frames were slow (${Math.round(rate)} a second), so the game is now drawn at ${drawn()} and scaled up to the screen.`);
794
+ }
795
+ }
796
+
797
+ // Fits the canvas to the window at the console's shape, black around it.
798
+ function fit() {
799
+ const canvas = renderer.domElement, scale = Math.min(innerWidth / WIDTH, innerHeight / HEIGHT);
800
+ canvas.style.cssText = `position:fixed;left:${(innerWidth - WIDTH * scale) / 2}px;top:${(innerHeight - HEIGHT * scale) / 2}px;width:${WIDTH * scale}px;height:${HEIGHT * scale}px`;
801
+ }
802
+
803
+ // The game: `renderer` a three.js WebGLRenderer the runtime sizes and places, `init` run once (and waited for, so it may
804
+ // load files), `frame(f)` each frame with f.dt, f.time, f.number and f.pads (pads[0] the first controller: lx, ly, rx,
805
+ // ry -1 to 1 with up positive, lt and rt 0 to 1, buttons as BUTTON bits in down, pressed and released) and f.phones (one
806
+ // { player, down, pressed, released } for each phone joined through Lamp.controllers, buttons by the names the phone
807
+ // page gives them: `p.pressed.a` is true for the one frame after a press, even one let go again since). `touch` names
808
+ // the buttons the touch controls show (BUTTON.A | BUTTON.B | BUTTON.MENU unless given; BUTTON.RSTICK adds a right stick),
809
+ // or `false` for a page that draws touch controls of its own. `keyboard: false` is for a page that reads the keys itself:
810
+ // the keyboard then isn't the first pad as well, so a key isn't heard twice. Lamp's own controller is the first pad either way.
811
+ // `keyboardHints: { leftStick: 'arrows' }` is for a game whose person asked for the arrow keys: label() and glyph() name
812
+ // them for the left stick instead of W A S D ('wasd', the default). It only chooses the name: both keep moving the stick.
813
+ // The renderer may be a WebGLRenderer or, from 'three/webgpu', a WebGPURenderer (initialized before game() is called). A 2D
814
+ // game calls phaser() from lamp-phaser.js instead, which calls this with Phaser's canvas.
815
+ // `sharp: true` keeps it drawn at 1920x1080 however slow frames are (the log still says how fast they come).
816
+ // `fill: true` is for a page laid out at the window's size, as a web page is (a strategy game whose HUD is the page's own
817
+ // HTML, made for phones as well as TVs): the page puts the canvas where it wants it and sizes it itself, drawing it
818
+ // smaller when frames run slow, and the runtime only measures and logs the frame rate.
819
+ // `snapshot`, `restore`, `busy`, `assetChanged` and `dispose` keep the player's place through a change Lamp makes while
820
+ // the game is played (see "Changes while it's played" below).
821
+ export async function game({ renderer: given, init, frame, touch: buttons, keyboard = true, keyboardHints, sharp = false, fill = false,
822
+ snapshot, restore, busy, assetChanged, dispose, flat = false }) {
823
+ // The game's code run again in place for a change calls this again: the page, the frame loop's clock and whatever
824
+ // the game kept stay, and the new init starts from where the player was.
825
+ const again = runningAgain;
826
+ if (again) again.called = true;
827
+ const old = renderer;
828
+ renderer = given;
829
+ watchRender(renderer);
830
+ fills = fill;
831
+ adapting = !sharp && !fill;
832
+ // Drawn at full size from here (setSize below), however small the code run before drew it.
833
+ level = 0; rateBefore = 0; check.times = []; check.time = 0; check.warm = false;
834
+ frameFunction = frame;
835
+ hooks = { snapshot, restore, busy, assetChanged, dispose };
836
+ flatGame = flat;
837
+ asksPlace = false;
838
+ // A renderer the new code didn't keep goes with the old code (its canvas went with the old page).
839
+ if (again && old && old !== given && !isKept(old)) { try { old.dispose && old.dispose(); } catch (e) {} }
840
+ if (buttons === false) drawsTouch = false;
841
+ else if (buttons != null) touchButtons = buttons;
842
+ readsKeyboard = keyboard !== false;
843
+ // Set on every call, so code run again for a change that takes the hint away goes back to the default. Prompts drawn
844
+ // before this call are drawn again.
845
+ const hint = LEFT_STICK_HINTS[keyboardHints && keyboardHints.leftStick] ?? LEFT_STICK_HINTS.wasd;
846
+ if (hint !== leftStickHint) { leftStickHint = hint; drawPrompts(); }
847
+ if (!fill) {
848
+ renderer.setPixelRatio(1);
849
+ renderer.setSize(WIDTH, HEIGHT, false);
850
+ }
851
+ document.documentElement.style.background = document.body.style.background = '#000';
852
+ document.body.style.margin = '0';
853
+ document.body.style.overflow = 'hidden';
854
+ // A game's page is played, not read: nothing on it is selected, magnified or called up by a long press.
855
+ document.body.style.webkitUserSelect = document.body.style.userSelect = 'none';
856
+ document.body.style.webkitTouchCallout = 'none';
857
+ document.body.style.touchAction = 'none';
858
+ if (!fill) {
859
+ document.body.append(renderer.domElement);
860
+ fit();
861
+ if (!fitting) { fitting = true; listen(window, 'resize', fit); }
862
+ }
863
+ // Where the player was, if the page is opened again for a change: known before init, so the game starts there.
864
+ if (again) { pending = { state: again.state && again.state.state, held: again.state && again.state.held, gen: again.change.gen }; reopened = true; taken = false; }
865
+ else { await claim(); keepPage(); }
866
+ // What the game registered takes its place before init reads it.
867
+ if (pending && pending.held) {
868
+ restoring = pending.held;
869
+ if (!again) guarding = true;
870
+ for (const entry of states.values()) settle(entry);
871
+ }
872
+ if (init) {
873
+ try { await init({ width: WIDTH, height: HEIGHT }); } catch (e) {
874
+ if (again) return ranAgainFailed(again, e);
875
+ if (taken || guarding) return startFresh(e);
876
+ log(`Error: ${e && e.message || e}`);
877
+ throw e;
878
+ }
879
+ }
880
+ // Whatever the start set on a camera, it's where the player left it at the first frame.
881
+ if (restoring) for (const entry of cameras.values()) putPose(entry, restoring.cameras[entry.id]);
882
+ await putBack();
883
+ if (again) ranAgain(again);
884
+ // The first frame of code run again has its shaders compiled before it's drawn (`watchRender`).
885
+ if (again && renderer.__lampRender) warming = 'ask';
886
+ // One loop at a time: the game's code run again starts a new one, and the old one stops at its next frame.
887
+ const mine = ++loops;
888
+ reopenedFrame = 0;
889
+ let last = 0;
890
+ const loop = now => {
891
+ if (mine !== loops) return;
892
+ // Its shaders compiling: the frame before stays on screen, and the game waits for them.
893
+ if (warming && warming !== 'ask') { nextFrame(loop); return; }
894
+ const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
895
+ // How long the page took to build the game, for the log.
896
+ const first = !last && mine === 1;
897
+ if (first) log(`Lamp: the game was built and drawn ${(now / 1000).toFixed(1)} s after the page opened.`);
898
+ last = now;
899
+ measure(now);
900
+ drewThisFrame = false;
901
+ framesDrawn++;
902
+ const began = clockNow();
903
+ try { step(dt); } catch (e) {
904
+ // New code swapped in or run again that fails as it runs goes back out the safe way: the page opens again
905
+ // from where the player was before it.
906
+ if (swapGuard) { log(`Error: ${e && e.message || e}`); return swapFailed(e); }
907
+ throw e;
908
+ }
909
+ // The first frame drawn from a snapshot has gone without an error: from here an error is the game's own.
910
+ guarding = false;
911
+ // A frame that drew nothing through the renderer has nothing to compile.
912
+ if (warming === 'ask') warming = null;
913
+ const cost = clockNow() - began;
914
+ frameCost = frameCost ? frameCost * 0.9 + cost * 0.1 : cost;
915
+ if (reporting && !warming) firstDrawn(cost);
916
+ shoot();
917
+ // The first frame of play is drawn, and on screen at the next: the app's picture for Lamp's desk is taken from
918
+ // there, past the loading screen (`tellPlaying`).
919
+ if (first) nextFrame(() => tellPlaying('playing'));
920
+ // Opened again for a change: it's on screen with this first frame.
921
+ if (shownFor != null && !curtain && !runningAgain && reopenedFrame++ > 0) { tellHost(shownFor, 'shown'); shownFor = null; }
922
+ lift();
923
+ live();
924
+ nextFrame(loop);
925
+ };
926
+ nextFrame(loop);
927
+ }
928
+
929
+ // MARK: Changes while it's played
930
+ //
931
+ // Lamp changes the game while the person plays it, and puts each change into the running game when it can, so the
932
+ // world it built (the scene, the renderer, geometry, compiled shaders) and the player stay as they are. A game says
933
+ // what's to stay by registering it (see "What the game registers"):
934
+ //
935
+ // - Files the game loaded through the runtime (asset(), texture(), model(), sound.load()): the runtime puts a new copy
936
+ // of what it loaded in itself, and then calls the game's `assetChanged(path)` for each changed file, which replaces
937
+ // anything of the game's own made from it (a material from a shader's source, a clone of a model, a level read from
938
+ // JSON) and returns true when it has.
939
+ // - A stylesheet the page links: the new one replaces it.
940
+ // - A module of the game's that takes its new versions: one that calls `hot(import.meta).accept()` (below). Its new
941
+ // version runs, and what it exports is put into what the running game holds: each exported class gets the new
942
+ // methods (so every object made from it, made before or after, runs the new code with its own fields kept), each
943
+ // exported plain object the new functions and the values the change changed (its other values, which the game may
944
+ // have changed as it ran, stay), and each exported array the new contents when the change changed them. A module that doesn't take its changes is swapped through the modules that import
945
+ // it: when every one of them, or one above it, takes them, it runs again for them, and they take theirs.
946
+ //
947
+ // Anything else runs the game's code again in place, keeping the player's place, the page, the renderer and what the
948
+ // game kept (see "Running the game's code again" below): the HTML, a module nothing above takes changes for (the one
949
+ // that starts the game, as a rule), a class whose constructor or fields changed (objects already made have the old
950
+ // fields) or that has private members (#name), an exported function or value that changed (whoever imported it holds
951
+ // the old one), or code a module's exported functions may use, changed outside its classes. Where that can't be done
952
+ // (a change to the import map, modules that import each other in a circle) the page opens again. The log says which,
953
+ // and why. Just before, what the game registered is held, and a game from before registering has `snapshot()`, which
954
+ // gives what matters of where the game is (the level, the player's position, the camera, the score, the mode, which
955
+ // menu or overlay is open…) as plain JSON. That game's new code starts straight there: `pendingRestore()` gives that state while the game starts (in init, or in a Phaser scene's
956
+ // init or create), so the game builds the place it was in rather than its title or intro, and the runtime calls
957
+ // nothing more. A game that doesn't read it and has `restore(state)` is started as usual and then given the state,
958
+ // before its first frame is drawn. A game without either starts again, from its own save() and load(). If starting
959
+ // from the state throws (init, a scene's create, restore or the first frame), the game starts fresh, and the log says
960
+ // so. New code swapped in or run again that throws in its first second opens the page again from the place before.
961
+ // While `busy()` is true (a match or a round under way, a race, a fight) a change that runs the code again waits, and
962
+ // goes in at the next moment busy() is false (a pause, between rounds); the person can also put it in at once. A swap
963
+ // goes in at once, busy or not. Where the app around the page has no Reload of Lamp's own, the page shows a small
964
+ // "Reload to apply" to press.
965
+ // Wherever else the game is played (Android, the Xbox, a TV, a phone), a change reaches it when Lamp installs it there
966
+ // again: the runtime keeps the latest snapshot, and the new copy restores it once, when it opens.
967
+ //
968
+ // A module that takes its changes ends with
969
+ //
970
+ // hot(import.meta).accept();
971
+ //
972
+ // and keeps its state in objects made from the classes it exports (the game holds them: `new Traffic(scene)`), with
973
+ // its tuning in constants or plain objects it reads where it uses them, rather than copied into fields as an object is
974
+ // made: what a constructor built or copied stays as it was until the code runs again. Everything a module defines
975
+ // that the game calls goes out through its exports, classes above all; a class it keeps to itself keeps its old
976
+ // methods. `accept(next, { changed })` takes a function, called once the new version is in, with its exports and the
977
+ // names of what changed in them (`Traffic.update`, `TUNING.topSpeed`); it does what the change needs done that the
978
+ // swap doesn't (build again what a changed method built once), and returns false to have the code run again
979
+ // instead. For what a module makes at its top level (a listener, an element, a timer), `hot(import.meta).dispose(data
980
+ // => …)` takes it away as the old version goes, keeping in `data` what the next needs, which reads it as
981
+ // `hot(import.meta).data`. Nothing here runs where changes come by an install.
982
+
983
+ let hooks = {}, started = false, waiting = null, pill = null;
984
+ // The place this copy starts in (claim()), whether the game read it (pendingRestore()), and whether an error now means
985
+ // the state didn't take.
986
+ let pending = null, reopened = false, taken = false, guarding = false;
987
+ const host = () => window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.lampLive;
988
+ // Whether the game is past its loading screen, for the picture of the app Lamp's desk shows (`LampPagePlaying` in a Mac
989
+ // app): 'loading' from when this runtime loads, 'playing' at the first frame of play.
990
+ const tellPlaying = state => { try { const h = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.lampPlaying; h && h.postMessage(state); } catch (e) {} };
991
+ tellPlaying('loading');
992
+ // Putting in what was built in the background says only its report: the change it's part of has said the rest.
993
+ const tellHost = (gen, state, why, more) => { if (runningAgain && runningAgain.change.quiet && state !== 'report') return; try { const h = host(); h && h.postMessage({ gen, state, ...(why ? { why } : {}), ...more }); } catch (e) {} };
994
+ // What a change of these is, is code: swapped in, or the page is opened again for it.
995
+ const CODE = /\.(m?js|html?|css)$/i;
996
+ // Each copy of the page names its build (lamp-build.js, written with lamp-device.js), so a snapshot is restored only by a
997
+ // newer build. A copy without one restores only what the page itself kept for its reopening.
998
+ let buildID = null;
999
+ const build = import('./lamp-build.js').then(m => (buildID = m.default), () => null);
1000
+ const LIVE = 'lamp.live', LAST = 'lamp.live.last';
1001
+ const store = kind => { try { return kind === 'session' ? sessionStorage : localStorage; } catch (e) { return null; } };
1002
+
1003
+ // Where the game is: `state` what its snapshot() gave, `held` what it registered (`heldNow`); null when it has neither.
1004
+ function takeSnapshot() {
1005
+ const held = heldNow();
1006
+ let state = null;
1007
+ try {
1008
+ const given = hooks.snapshot ? hooks.snapshot() : undefined;
1009
+ if (given !== undefined) state = JSON.parse(JSON.stringify(given));
1010
+ } catch (e) { log(`Lamp: snapshot() failed (${e && e.message || e}).`); }
1011
+ return state == null && !held ? null : { state, held };
1012
+ }
1013
+
1014
+ function isBusy() {
1015
+ try { return !!(hooks.busy && hooks.busy()); } catch (e) { return false; }
1016
+ }
1017
+
1018
+ // Lamp's app around the page hands it a change: `changed` the files changed, `gen` its number, `now` when the person
1019
+ // pressed Reload, `pill` when the page is to show its own Reload. Returns true: the page takes changes. One change is
1020
+ // put in at a time, so one arriving during a swap waits for it.
1021
+ let queue = Promise.resolve();
1022
+ window.__lampLive = change => {
1023
+ changes = Math.max(changes + 1, change.gen || 0);
1024
+ queue = queue.then(() => {
1025
+ // What the code run again couldn't take in goes in as the page opens again, at Reload.
1026
+ if (waiting && waiting.reopens && change.now) { waiting = null; return reopen(change); }
1027
+ // A file taken away while a change waits, and made again since, is a changed file like any other.
1028
+ if (waiting) change = { ...change, changed: [...new Set([...waiting.changed, ...change.changed])],
1029
+ removed: [...new Set([...(waiting.removed || []).filter(p => !change.changed.includes(p)), ...(change.removed || [])])] };
1030
+ waiting = null;
1031
+ return apply(change);
1032
+ }).catch(e => log(`Lamp: ${e && e.message || e}`));
1033
+ return true;
1034
+ };
1035
+
1036
+ // The app menu's Reload with no change waiting: the game opens again from its files, every part of it built afresh, with
1037
+ // the player where they were.
1038
+ // After any change being put in (one at a time, `queue`), with the place a rerun under way kept for itself, never one
1039
+ // taken from a game it has half started again.
1040
+ window.__lampReloadNow = () => {
1041
+ const held = runningAgain && runningAgain.state;
1042
+ queue = queue.then(() => reopen({ gen: changes, why: 'Reload' }, held || takeSnapshot()))
1043
+ .catch(e => log(`Lamp: ${e && e.message || e}`));
1044
+ return true;
1045
+ };
1046
+
1047
+ async function apply(change) {
1048
+ let paths = (change.changed || []).map(clean);
1049
+ // Not started yet: nothing to keep.
1050
+ if (!started) { tellHost(change.gen, 'reloading'); location.reload(); return; }
1051
+ // Where the player is before anything is swapped, for the page to open from should the swap not take.
1052
+ const before = takeSnapshot(), roster = phonesHere(), began = performance.now();
1053
+ // Files taken away that the running game has no use for are no part of the change. With nothing else in it there's
1054
+ // nothing to put in, and the game goes on as it was.
1055
+ const unused = await unusedFiles(change, paths);
1056
+ if (unused.length) {
1057
+ for (const path of unused) texts.delete(path);
1058
+ paths = paths.filter(p => !unused.includes(p));
1059
+ change = { ...change, changed: paths };
1060
+ log(`Lamp: ${unused.join(', ')} ${unused.length === 1 ? 'was' : 'were'} taken away, and the running game has no use for ${unused.length === 1 ? 'it' : 'them'}.`);
1061
+ if (!paths.length) { hidePill(); tellHost(change.gen, 'swapped'); return; }
1062
+ }
1063
+ const said = [];
1064
+ let why = paths.length ? null : 'nothing to put in', touched = false;
1065
+ // A part declared as built from a changed file is built again, with everything else kept: a swap would leave it as
1066
+ // it was built.
1067
+ if (paths.some(p => /\.m?js$/i.test(p)) && [...kept.values()].some(k => k.from.length)) {
1068
+ try { await readModules(paths.filter(p => /\.m?js$/i.test(p))); } catch (e) {}
1069
+ }
1070
+ const own = builtFrom(paths);
1071
+ if (own) why = `${own.path} changed, which ${own.key} is built from`;
1072
+ for (const path of why ? [] : paths.filter(p => !CODE.test(p))) {
1073
+ let done = false;
1074
+ for (const swap of swaps.get(path) || []) { try { await swap(); done = true; } catch (e) { log(`Lamp: couldn't put in the new ${path} (${e && e.message || e}).`); } }
1075
+ if (hooks.assetChanged) { try { if (await hooks.assetChanged(path)) done = true; } catch (e) { log(`Error: assetChanged('${path}'): ${e && e.message || e}`); } }
1076
+ if (done) said.push(path);
1077
+ else why = why || (loaded.has(path) ? `${path} changed, and nothing in the running game takes the new one in`
1078
+ : `nothing the game loaded through lamp-game.js is made from ${path}`);
1079
+ }
1080
+ const code = paths.filter(p => CODE.test(p));
1081
+ if (!why && code.length) {
1082
+ const swapped = await swapCode(code);
1083
+ if (swapped.why) { why = swapped.why; touched = swapped.touched; } else said.push(...swapped.said);
1084
+ }
1085
+ if (!why) {
1086
+ log(`Lamp: put in ${said.join('; ')} while the game ran (${Math.round(performance.now() - began)} ms).`);
1087
+ if (code.length) { swapGuard = { until: time + 1, change, state: before }; markStale(code); }
1088
+ tellHost(change.gen, 'swapped');
1089
+ return;
1090
+ }
1091
+ change = { ...change, why, put: said };
1092
+ // A run that would start the game over waits for the person: only they can say the place is worth losing.
1093
+ if (!keepsPlace() && !change.now && !touched) {
1094
+ const unkept = unregistered();
1095
+ log(`Lamp: a change that runs the game's code again would start play over, since the game hasn't registered ${unkept.join(', ')}.`);
1096
+ return reopenOrHold({ ...change, why: `${why}; that would start play over, since the game hasn't registered ${unkept.join(', ')}` }, undefined);
1097
+ }
1098
+ // Half swapped is no place to wait in: the code runs again now, from where the player was before.
1099
+ if (isBusy() && !change.now && !touched) {
1100
+ waiting = change;
1101
+ log(`Lamp: a change that needs the game's code run again (${why}) waits for a pause in the game.`);
1102
+ tellHost(change.gen, 'held', why, { pause: true });
1103
+ if (change.pill) showPill();
1104
+ return;
1105
+ }
1106
+ return startAgain(touched ? { ...change, touched } : change, touched ? before : undefined, touched ? roster : undefined);
1107
+ }
1108
+
1109
+ // A change that can't be swapped in: the game's code runs again in place, keeping the page, the renderer and what the
1110
+ // game kept, or, where that can't be done, the page opens again. Either way from `state`, where the player was, and
1111
+ // `roster`, the phones joined as it was taken: the new run hears of any who left since, whose place `state` still has.
1112
+ function startAgain(change, state = takeSnapshot(), roster = phonesHere()) {
1113
+ const why = canRunAgain(change);
1114
+ if (why) return reopenOrHold({ ...change, why: `${change.why}; ${why}` }, state);
1115
+ return runAgain(change, state, roster);
1116
+ }
1117
+
1118
+ // A change that needs the page opened again is held for the person's Reload, never opened under them: the app and the
1119
+ // chat show Reload (`held`), and it goes in when they press it (`now`). The code run again in place goes in by itself.
1120
+ // A swap that already changed the running game (its old version's disposers run, its exports put in) leaves nothing safe
1121
+ // to wait in: that opens the page again at once, from where the player was before it.
1122
+ function reopenOrHold(change, state) {
1123
+ if (change.now || change.touched) return reopen(change, state);
1124
+ waiting = { ...change, hard: true };
1125
+ log(`Lamp: a change that needs the game opened again (${change.why}) waits for Reload.`);
1126
+ tellHost(change.gen, 'held', change.why);
1127
+ if (change.pill) showPill();
1128
+ }
1129
+
1130
+ // Opens the page again with the new code, with the player's place kept for the new code to restore: where it is now, or
1131
+ // `state` taken before a swap that didn't take.
1132
+ function reopen(change, snap = takeSnapshot()) {
1133
+ hidePill();
1134
+ try { store('session').setItem(LIVE, JSON.stringify({ ...snap, input: current, padName, gen: change.gen })); } catch (e) {}
1135
+ const why = change.why ? ` (${change.why})` : '';
1136
+ log(snap == null ? `Lamp: opening the game again for a change of its code${why}; it has no snapshot(), so it starts from its own saves.`
1137
+ : `Lamp: opening the game again for a change of its code${why}, keeping where the player was.`);
1138
+ tellHost(change.gen, 'reloading', change.why);
1139
+ location.reload();
1140
+ }
1141
+
1142
+ // Each frame: a waiting change goes in once the game isn't busy; the latest snapshot is kept for the next install.
1143
+ let keptAt = 0;
1144
+ function live() {
1145
+ if (swapGuard && time > swapGuard.until) swapGuard = null;
1146
+ if (swapWaits && !isBusy() && !runningAgain) { swapWaits = false; swapIn(); }
1147
+ if (waiting && !waiting.hard && !isBusy()) { const change = waiting; waiting = null; queue = queue.then(() => startAgain(change)).catch(e => log(`Lamp: ${e && e.message || e}`)); }
1148
+ if ((hooks.snapshot || cameras.size || states.size) && time - keptAt >= 5) { keptAt = time; keepLast(); }
1149
+ }
1150
+
1151
+ // MARK: Swapping code in
1152
+ //
1153
+ // What the runtime knows of the game's modules: each that called hot(), by its path beside the page, with every version
1154
+ // of it that has run (its exports, and the values of its plain objects as each version made them, to tell what a
1155
+ // change changed from what the game changed as it ran), its source as the last version ran, and the address its
1156
+ // latest version was loaded from, which a module loaded afresh imports it by.
1157
+ const modules = new Map(), latest = new Map(), loading = new Set();
1158
+ // The swap that went in last, watched for its first second: an error in it opens the page again.
1159
+ let swapGuard = null, versionCount = 0;
1160
+ // Where the page's files are, read when first needed: a copy of the runtime run outside a page has no URL.
1161
+ let base = null;
1162
+ const pageBase = () => base || (base = new URL('.', location.href));
1163
+ const pathOf = url => {
1164
+ try {
1165
+ const u = new URL(url, location.href), b = pageBase();
1166
+ if (u.protocol !== b.protocol || u.host !== b.host || !u.pathname.startsWith(b.pathname)) return null;
1167
+ return decodeURIComponent(u.pathname.slice(b.pathname.length));
1168
+ } catch (e) { return null; }
1169
+ };
1170
+ const addressOf = path => new URL(path, pageBase()).href;
1171
+ const source = f => Function.prototype.toString.call(f);
1172
+ const isClass = v => typeof v === 'function' && /^class\b/.test(source(v));
1173
+ const isPlain = v => !!v && typeof v === 'object' && [Object.prototype, null].includes(Object.getPrototypeOf(v));
1174
+ const valueText = v => { try { return typeof v === 'function' ? 'function ' + source(v) : JSON.stringify(v) ?? String(v); } catch (e) { return String(v); } };
1175
+ // Code as it reads with its comments and spacing gone, so a comment added beside a method changes nothing.
1176
+ const bare = text => text.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, '').replace(/\s+/g, '');
1177
+
1178
+ // A module's handle on its changes while the game is played (see "Changes while it's played" above): `accept()` takes
1179
+ // them, `dispose(fn)` takes away what this version made as it goes, `data` is what the last version's disposers kept,
1180
+ // `keep(key, build, { inputs, from, background })` a part of the game kept when its code runs again (see "Running the game's code
1181
+ // again" and "Building in the background"), and
1182
+ // `renderer(build)`, `camera(id, build)` and `state(id, { version, start })` what the runtime keeps for the game (see
1183
+ // "What the game registers").
1184
+ export function hot(meta) {
1185
+ const path = pathOf(meta && meta.url);
1186
+ // A page that takes no changes still restores what's registered when a newer install opens it.
1187
+ const quiet = { data: {}, accept() {}, dispose() {}, renderer: build => build(), camera, state,
1188
+ keep(key, build, options) { const v = build(); return options && options.background && isSteps(v) ? stepThrough(v) : v; } };
1189
+ if (path == null || !host()) return quiet;
1190
+ let entry = modules.get(path);
1191
+ if (!entry) modules.set(path, entry = { path, accepts: false, onAccept: null, disposers: [], data: {}, versions: [], text: null });
1192
+ // Each version says for itself whether it takes the next, and what to take away when it goes.
1193
+ entry.accepts = false;
1194
+ entry.onAccept = null;
1195
+ entry.disposers = [];
1196
+ if (!loading.has(path) && !entry.versions.length) {
1197
+ // The version the page loaded first, which every module that imported it holds: known once it has run.
1198
+ // A module that awaits at its top level can be handed over before it has finished: its exports can't be read
1199
+ // yet, so it's asked again.
1200
+ const first = tries => import(meta.url).then(ns => { if (!entry.versions.length) addVersion(entry, versionOf(ns)); })
1201
+ .catch(e => { if (e instanceof ReferenceError && tries) after(250, () => first(tries - 1)); });
1202
+ first(40);
1203
+ fetch(meta.url, { cache: 'no-store' }).then(a => a.ok ? a.text() : null).then(t => { if (entry.text == null) entry.text = t; }, () => {});
1204
+ }
1205
+ return {
1206
+ data: entry.data,
1207
+ accept(callback) { entry.accepts = true; entry.onAccept = typeof callback === 'function' ? callback : null; },
1208
+ dispose(callback) { if (typeof callback === 'function') entry.disposers.push(callback); },
1209
+ keep(key, build, options) { return keep(path, key, build, options); },
1210
+ renderer(build) { return keep('', 'renderer', build, { lasting: true }); },
1211
+ camera, state,
1212
+ };
1213
+ }
1214
+
1215
+ // Only the last version's text is compared with: an array's can be megabytes, for every run of the code.
1216
+ function addVersion(m, version) {
1217
+ const last = m.versions[m.versions.length - 1];
1218
+ if (last) last.made = null;
1219
+ m.versions.push(version);
1220
+ }
1221
+ function versionOf(ns) {
1222
+ const made = {};
1223
+ for (const name of Object.keys(ns)) {
1224
+ if (Array.isArray(ns[name])) made[name] = valueText(ns[name]);
1225
+ if (!isPlain(ns[name])) continue;
1226
+ made[name] = {};
1227
+ for (const key of Object.keys(ns[name])) made[name][key] = valueText(ns[name][key]);
1228
+ }
1229
+ return { ns, made };
1230
+ }
1231
+
1232
+ // The game's modules, read from the page: what each imports (static imports, re-exports and import() of a written-out
1233
+ // path), so a change can go up to the modules that take it. Kept between changes, and read again for what changed.
1234
+ const imports = new Map(), texts = new Map();
1235
+ const SPECIFIER = /(\bimport\s*\(\s*|\bfrom\s*|\bimport\s+)(['"])([^'"\n]+)\2/g;
1236
+ // Lamp's own files beside the page never change with the game, and three.js is large: they aren't read.
1237
+ const lampOwn = path => /^(three|rapier|phaser)\//.test(path) || /^lamp(-[\w-]+)?\.js$/.test(path);
1238
+ let importMap = null;
1239
+ function resolve(specifier, from) {
1240
+ if (/^\.{0,2}\//.test(specifier)) return new URL(specifier, from).href;
1241
+ if (/^[a-z][\w+.-]*:/i.test(specifier)) return specifier;
1242
+ if (!importMap) {
1243
+ importMap = {};
1244
+ for (const s of document.querySelectorAll('script[type="importmap"]')) { try { Object.assign(importMap, JSON.parse(s.textContent).imports || {}); } catch (e) {} }
1245
+ }
1246
+ if (importMap[specifier]) return new URL(importMap[specifier], location.href).href;
1247
+ const prefix = Object.keys(importMap).filter(k => k.endsWith('/') && specifier.startsWith(k)).sort((a, b) => b.length - a.length)[0];
1248
+ return prefix ? new URL(importMap[prefix] + specifier.slice(prefix.length), location.href).href : null;
1249
+ }
1250
+ // A specifier nothing resolves (a bare name the import map doesn't have) names no file of the game's.
1251
+ const importsIn = (text, from) => [...text.matchAll(SPECIFIER)].map(m => resolve(m[3], from)).filter(Boolean).map(pathOf)
1252
+ .filter(p => p != null && !lampOwn(p));
1253
+ // The names a module's imports give it, each with the module it comes from: `a`, `b` and `ns` in
1254
+ // `import a, { x as b } from './m.js'` and `import * as ns from './n.js'`.
1255
+ const BINDINGS = /\bimport\s+([\w$*{}\s,]+?)\s+from\s*(['"])([^'"\n]+)\2/g;
1256
+ const bindings = new Map();
1257
+ function bindingsIn(text, from) {
1258
+ const names = new Map();
1259
+ for (const m of text.matchAll(BINDINGS)) {
1260
+ const target = resolve(m[3], from), path = target && pathOf(target);
1261
+ if (path == null || lampOwn(path)) continue;
1262
+ const clause = m[1].replace(/\*\s*as\s+/, '').replace(/[{}]/g, ',');
1263
+ for (const part of clause.split(',')) { const name = part.trim().split(/\s+as\s+/).pop(); if (name) names.set(name, path); }
1264
+ }
1265
+ return names;
1266
+ }
1267
+
1268
+ async function readText(path) {
1269
+ const answer = await fetch(`${addressOf(path)}?lamp=${changes}`, { cache: 'no-store' });
1270
+ if (!answer.ok) throw new Error(`${path} couldn't be read`);
1271
+ return answer.text();
1272
+ }
1273
+
1274
+ // `page` is the document whose module scripts start the game: the running page, or the page's HTML as it is now when
1275
+ // the game's code runs again. The page's own scripts (inline) are kept by name in `inline`.
1276
+ let rootPaths = [];
1277
+ const inline = new Map();
1278
+ async function readModules(changed, page = document) {
1279
+ for (const path of changed) imports.delete(path);
1280
+ const roots = [];
1281
+ for (const s of page.querySelectorAll('script[type="module"]')) {
1282
+ const src = s.getAttribute('src');
1283
+ const path = src ? pathOf(new URL(src, location.href).href) : `the page's own script ${roots.length + 1}`;
1284
+ if (path == null) continue;
1285
+ if (!src) { imports.set(path, importsIn(s.textContent, location.href)); bindings.set(path, bindingsIn(s.textContent, location.href)); inline.set(path, s.textContent); }
1286
+ roots.push(path);
1287
+ }
1288
+ rootPaths = roots;
1289
+ const unread = () => [...new Set([...roots, ...[...imports.values()].flat()])].filter(p => !imports.has(p));
1290
+ let next = unread();
1291
+ while (next.length) {
1292
+ await Promise.all(next.map(async path => {
1293
+ const text = await readText(path).catch(() => '');
1294
+ if (changed.includes(path)) texts.set(path, text);
1295
+ imports.set(path, importsIn(text, addressOf(path)));
1296
+ bindings.set(path, bindingsIn(text, addressOf(path)));
1297
+ }));
1298
+ next = unread();
1299
+ }
1300
+ return roots;
1301
+ }
1302
+
1303
+ // Whether `text` names a file: by its whole name, so `world.js` doesn't name `orld.js`. Only a name: a comment naming the
1304
+ // file counts, and two files of one name in different folders count as each other, which errs toward saying a file is used.
1305
+ function names(text, path) {
1306
+ const name = path.split('/').pop().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1307
+ return new RegExp(`(^|[^\\w$.-])${name}($|[^\\w$-])`).test(text);
1308
+ }
1309
+
1310
+ // What of a change's files taken away the running game clearly has no use for: nothing it runs imports them, it never read
1311
+ // them, no part is built from them, and neither its code, its page nor its stylesheets name them. Anything else (a
1312
+ // worker it started, data it fetched, a module it imports, a name in a comment) is left in the change, which goes on to
1313
+ // say honestly what came of it. With a worker started, what's read is only partly known, so no file is known to be
1314
+ // unused, and a page that can't be read says the same. Not for the person's Reload, which puts everything in.
1315
+ async function unusedFiles(change, paths) {
1316
+ const removed = (change.removed || []).map(clean).filter(p => paths.includes(p));
1317
+ if (!removed.length || change.now || workerStarted) return [];
1318
+ try {
1319
+ if (removed.includes(pagePath())) return [];
1320
+ const html = await readText(pagePath()), parsed = parse(html);
1321
+ await readModules(paths.filter(p => /\.m?js$/i.test(p)), parsed);
1322
+ const styles = [...parsed.querySelectorAll('link[rel~="stylesheet"]')].map(l => pathOf(new URL(l.getAttribute('href'), location.href).href));
1323
+ const asked = removed.filter(p => !lampOwn(p) && !imports.has(p) && !loaded.has(p) && !everRead.has(p) && !swaps.has(p) && !builtFrom([p]));
1324
+ const sources = [html];
1325
+ for (const path of [...imports.keys(), ...styles]) {
1326
+ if (path == null || lampOwn(path) || removed.includes(path)) continue;
1327
+ sources.push(texts.get(path) ?? inline.get(path) ?? await readText(path));
1328
+ }
1329
+ return asked.filter(p => !sources.some(text => names(text, p)));
1330
+ } catch (e) { return []; }
1331
+ }
1332
+
1333
+ // Puts changed modules and stylesheets into the running game: { said } what went in, or { why } it couldn't, with
1334
+ // `touched` when something was already taken away or swapped, so the page must open again now.
1335
+ async function swapCode(paths) {
1336
+ const html = paths.find(p => /\.html?$/i.test(p));
1337
+ if (html) return { why: `${html} changed` };
1338
+ const code = paths.filter(p => /\.m?js$/i.test(p)), styles = paths.filter(p => /\.css$/i.test(p));
1339
+ const said = [];
1340
+ if (code.length) {
1341
+ const swapped = await swapModules(code);
1342
+ if (swapped.why) return swapped;
1343
+ said.push(...swapped.said);
1344
+ }
1345
+ for (const path of styles) {
1346
+ const links = [...document.querySelectorAll('link[rel~="stylesheet"]')].filter(l => pathOf(l.href) === path);
1347
+ if (!links.length) return { why: `no link in the page loads ${path}`, touched: said.length > 0 };
1348
+ try {
1349
+ await Promise.all(links.map(old => new Promise((done, failed) => {
1350
+ const next = old.cloneNode();
1351
+ next.href = `${addressOf(path)}?lamp=${++versionCount}`;
1352
+ next.onload = () => { old.remove(); done(); };
1353
+ next.onerror = () => { next.remove(); failed(new Error(`${path} didn't load`)); };
1354
+ old.after(next);
1355
+ })));
1356
+ } catch (e) { return { why: e.message, touched: said.length > 0 }; }
1357
+ said.push(path);
1358
+ }
1359
+ return { said };
1360
+ }
1361
+
1362
+ async function swapModules(changed) {
1363
+ const roots = await readModules(changed);
1364
+ const importers = new Map();
1365
+ for (const [path, deps] of imports) for (const dep of deps) importers.set(dep, [...(importers.get(dep) || []), path]);
1366
+ const takes = path => { const m = modules.get(path); return !!(m && m.accepts && m.versions.length && m.text != null); };
1367
+ // Every module the change runs again, up to those that take it (the boundaries).
1368
+ const again = new Set(), boundaries = new Set();
1369
+ const up = path => {
1370
+ if (again.has(path)) return null;
1371
+ again.add(path);
1372
+ if (takes(path)) { boundaries.add(path); return null; }
1373
+ const by = importers.get(path) || [];
1374
+ // Nothing above takes it: the module that starts the game, or one nothing imports.
1375
+ if (!by.length) return roots.includes(path) ? { root: path } : { orphan: path };
1376
+ for (const above of by) { const stop = up(above); if (stop) return stop; }
1377
+ return null;
1378
+ };
1379
+ for (const path of changed) {
1380
+ if (lampOwn(path)) return { why: `Lamp's own ${path} changed` };
1381
+ if (!imports.has(path)) return { why: `nothing imports ${path}` };
1382
+ // The reason names the file that changed first: an `accept()` there is what would take the change, and the
1383
+ // module it's loaded through is only where the search for one ended.
1384
+ const stop = up(path);
1385
+ if (stop) {
1386
+ return { why: stop.orphan ? `nothing imports ${stop.orphan}`
1387
+ : stop.root === path ? `${path} doesn't take changes while the game runs`
1388
+ : `${path} doesn't take changes while the game runs, nor does ${stop.root}, which it's loaded through` };
1389
+ }
1390
+ }
1391
+ // Run again in order, what each imports before it.
1392
+ const order = [], seen = new Map();
1393
+ const visit = path => {
1394
+ if (seen.get(path) === 2) return true;
1395
+ if (seen.get(path) === 1) return false;
1396
+ seen.set(path, 1);
1397
+ for (const dep of imports.get(path) || []) if (again.has(dep) && !visit(dep)) return false;
1398
+ seen.set(path, 2);
1399
+ order.push(path);
1400
+ return true;
1401
+ };
1402
+ for (const path of again) if (!visit(path)) return { why: `${path} is in a circle of modules that import each other` };
1403
+
1404
+ // What the old versions made at their top level goes before the new ones run, as each asked.
1405
+ let touched = false;
1406
+ redeclared = null;
1407
+ for (const path of order) {
1408
+ const m = modules.get(path);
1409
+ for (const dispose of m ? m.disposers : []) {
1410
+ touched = true;
1411
+ try { await dispose(m.data); } catch (e) { log(`Error: dispose in ${path}: ${e && e.message || e}`); }
1412
+ }
1413
+ }
1414
+ // The new versions, each importing the others' new versions and the rest of the game as it runs: a module none of
1415
+ // whose imports changed is loaded from its own address, with a new query; one that imports another that runs again
1416
+ // is loaded from its source with those imports pointed at the new versions.
1417
+ // Each new version's exports as it made them, read before any of the game's values are put into them.
1418
+ const loaded = new Map(), addresses = new Map(), made = new Map();
1419
+ for (const path of order) {
1420
+ const address = `${addressOf(path)}?lamp=${++versionCount}`;
1421
+ const deps = imports.get(path) || [];
1422
+ loading.add(path);
1423
+ try {
1424
+ const text = texts.get(path) ?? await readText(path);
1425
+ texts.set(path, text);
1426
+ let from = address;
1427
+ if (deps.some(d => again.has(d) || latest.has(d))) {
1428
+ const written = text.replace(SPECIFIER, (all, lead, quote, specifier) => {
1429
+ const target = resolve(specifier, addressOf(path));
1430
+ const p = target && pathOf(target);
1431
+ if (!target) return all;
1432
+ return lead + quote + (p != null && addresses.has(p) ? addresses.get(p) : p != null && latest.has(p) ? latest.get(p) : target) + quote;
1433
+ }).replace(/\bimport\.meta\b/g, `({ url: ${JSON.stringify(address)}, resolve: s => new URL(s, ${JSON.stringify(address)}).href })`);
1434
+ from = URL.createObjectURL(new Blob([written], { type: 'text/javascript' }));
1435
+ }
1436
+ loaded.set(path, await import(from));
1437
+ // The new version's top level has run (listeners, elements): from here a refusal opens the page again at once.
1438
+ touched = true;
1439
+ made.set(path, versionOf(loaded.get(path)));
1440
+ addresses.set(path, from);
1441
+ } catch (e) {
1442
+ return { why: `${path} didn't load (${e && e.message || e})`, touched: true };
1443
+ } finally { loading.delete(path); }
1444
+ }
1445
+ if (redeclared) return { why: `the declaration of ${redeclared} changed`, touched };
1446
+ // Each boundary's new exports must fit what the game holds of the old ones.
1447
+ for (const path of boundaries) {
1448
+ const rerun = (imports.get(path) || []).find(d => again.has(d));
1449
+ const why = fits(modules.get(path), loaded.get(path), texts.get(path), path, rerun);
1450
+ if (why) return { why, touched };
1451
+ }
1452
+ const said = [];
1453
+ for (const path of boundaries) {
1454
+ const m = modules.get(path), ns = loaded.get(path);
1455
+ const changedNames = putIn(m, ns);
1456
+ addVersion(m, made.get(path));
1457
+ m.text = texts.get(path);
1458
+ if (m.onAccept) {
1459
+ try {
1460
+ if (await m.onAccept(ns, { changed: changedNames }) === false) return { why: `${path} asked for the page to be opened again`, touched: true };
1461
+ } catch (e) { return { why: `accept in ${path} failed (${e && e.message || e})`, touched: true }; }
1462
+ }
1463
+ said.push(changedNames.length ? `${path} (${changedNames.slice(0, 8).join(', ')}${changedNames.length > 8 ? '…' : ''})` : path);
1464
+ }
1465
+ for (const path of order) {
1466
+ latest.set(path, addresses.get(path));
1467
+ const m = modules.get(path);
1468
+ if (m && !boundaries.has(path)) { addVersion(m, made.get(path)); m.text = texts.get(path); }
1469
+ if (!boundaries.has(path)) said.push(`${path}, run again for the modules that import it`);
1470
+ }
1471
+ for (const path of changed) texts.delete(path);
1472
+ return { said };
1473
+ }
1474
+
1475
+ // Why the new exports can't be put into the old, or null when they can.
1476
+ function fits(m, ns, text, path, rerun) {
1477
+ const last = m.versions[m.versions.length - 1].ns;
1478
+ let holdsCode = false;
1479
+ for (const name of Object.keys(ns)) {
1480
+ const now = ns[name], was = last[name];
1481
+ // A new export: nothing holds an old one.
1482
+ if (was === undefined) continue;
1483
+ if (isClass(now) || isClass(was)) {
1484
+ if (!isClass(now) || !isClass(was)) return `${name} in ${path} is a class in one version and not the other`;
1485
+ if (shell(was) !== shell(now)) return `the constructor or fields of ${name} in ${path} changed`;
1486
+ // An object's private members belong to the class that made it: a method of a new version can't read them.
1487
+ if (PRIVATE.test(source(now))) return `${name} in ${path} has private members (#name), which a new version's methods can't reach`;
1488
+ } else if (typeof now === 'function' || typeof was === 'function') {
1489
+ if (typeof now !== 'function' || typeof was !== 'function' || source(now) !== source(was)) return `${path} exports the function ${name}, which changed`;
1490
+ holdsCode = true;
1491
+ } else if ((isPlain(now) && isPlain(was)) || (Array.isArray(now) && Array.isArray(was))) {
1492
+ continue;
1493
+ } else if (valueText(now) !== valueText(was)) {
1494
+ return `${path} exports ${name}, whose value changed`;
1495
+ } else if (now && typeof now === 'object') holdsCode = true;
1496
+ }
1497
+ if (holdsCode && rerun) return `${path} exports functions, which go on using the old ${rerun}`;
1498
+ if (holdsCode && outside(m.text, last) !== outside(text, ns)) return `code outside the classes in ${path} changed, which what it exports may use`;
1499
+ return null;
1500
+ }
1501
+
1502
+ // A use of a private member: `this.#speed`, `#speed in other`.
1503
+ const PRIVATE = /\.\s*#[A-Za-z_$]|#[A-Za-z_$][\w$]*\s+in\b/;
1504
+
1505
+ // A class as it reads without its methods, accessors and static methods: its constructor, fields and what it extends.
1506
+ function shell(C) {
1507
+ let text = source(C);
1508
+ for (const [from, skip] of [[C.prototype, ['constructor']], [C, ['length', 'name', 'prototype']]]) {
1509
+ for (const key of Reflect.ownKeys(from)) {
1510
+ if (skip.includes(key)) continue;
1511
+ const d = Object.getOwnPropertyDescriptor(from, key);
1512
+ for (const f of [d.value, d.get, d.set]) if (typeof f === 'function') { const s = source(f), i = text.indexOf(s); if (i >= 0) text = text.slice(0, i) + text.slice(i + s.length); }
1513
+ }
1514
+ }
1515
+ return bare(text);
1516
+ }
1517
+
1518
+ // A module's source without its exported classes.
1519
+ function outside(text, ns) {
1520
+ for (const value of Object.values(ns)) if (isClass(value)) { const s = source(value), i = text.indexOf(s); if (i >= 0) text = text.slice(0, i) + text.slice(i + s.length); }
1521
+ return bare(text);
1522
+ }
1523
+
1524
+ // Puts a new version's exports into every earlier version's, which the game holds; returns what changed.
1525
+ function putIn(m, ns) {
1526
+ const changed = new Set(), last = m.versions[m.versions.length - 1];
1527
+ for (const name of Object.keys(ns)) {
1528
+ const now = ns[name];
1529
+ const earlier = [...new Set(m.versions.map(v => v.ns[name]))].filter(v => v !== undefined && v !== now);
1530
+ if (isClass(now)) {
1531
+ for (const C of earlier) for (const key of putClass(C, now)) changed.add(`${name}.${key}`);
1532
+ // An object made by either version is an instance of both.
1533
+ const family = [...earlier, now];
1534
+ // A subclass inherits this static: it answers for its own class alone.
1535
+ function has(o) { return (family.includes(this) ? family : [this]).some(K => Function.prototype[Symbol.hasInstance].call(K, o)); }
1536
+ for (const K of family) Object.defineProperty(K, Symbol.hasInstance, { value: has, configurable: true });
1537
+ } else if (Array.isArray(now)) {
1538
+ // An array is taken whole: the change's new one when it changed it, else the one the game has.
1539
+ const held = earlier[earlier.length - 1];
1540
+ if (valueText(now) !== last.made[name] || !Array.isArray(held)) {
1541
+ changed.add(name);
1542
+ for (const A of earlier) if (Array.isArray(A)) A.splice(0, A.length, ...now);
1543
+ } else now.splice(0, now.length, ...held);
1544
+ } else if (isPlain(now)) {
1545
+ const before = last.made[name] || {}, held = earlier[earlier.length - 1];
1546
+ for (const key of Object.keys(now)) {
1547
+ const fresh = valueText(now[key]);
1548
+ if (fresh !== before[key]) changed.add(`${name}.${key}`);
1549
+ // What the change changed goes in; what it didn't keeps what the game made of it, in both copies.
1550
+ if (fresh !== before[key] || typeof now[key] === 'function' || !held || !(key in held)) for (const O of earlier) O[key] = now[key];
1551
+ else now[key] = held[key];
1552
+ }
1553
+ }
1554
+ }
1555
+ return [...changed];
1556
+ }
1557
+
1558
+ function putClass(C, N) {
1559
+ const changed = [];
1560
+ const code = d => d ? [d.value, d.get, d.set].filter(f => typeof f === 'function').map(source).join('\n') : '';
1561
+ for (const [from, to, skip] of [[N.prototype, C.prototype, ['constructor']], [N, C, ['length', 'name', 'prototype', Symbol.hasInstance]]]) {
1562
+ // A method the new version no longer has goes, so an old object falls back to what it inherits, as a new one does.
1563
+ for (const key of Reflect.ownKeys(to)) {
1564
+ if (skip.includes(key) || Object.prototype.hasOwnProperty.call(from, key)) continue;
1565
+ const d = Object.getOwnPropertyDescriptor(to, key);
1566
+ if (!d.get && !d.set && typeof d.value !== 'function') continue;
1567
+ delete to[key];
1568
+ changed.push(String(key));
1569
+ }
1570
+ for (const key of Reflect.ownKeys(from)) {
1571
+ if (skip.includes(key)) continue;
1572
+ const d = Object.getOwnPropertyDescriptor(from, key);
1573
+ if (!d.get && !d.set && typeof d.value !== 'function') continue;
1574
+ if (code(Object.getOwnPropertyDescriptor(to, key)) !== code(d)) changed.push(String(key));
1575
+ Object.defineProperty(to, key, d);
1576
+ }
1577
+ }
1578
+ return changed;
1579
+ }
1580
+
1581
+ // MARK: Running the game's code again
1582
+ //
1583
+ // A change that can't be swapped in (the HTML, the module that starts the game, a class whose fields changed, an
1584
+ // exported function) runs the game's code again in place, as a page opened again would but without opening it: the old
1585
+ // code's frame loop stops, its `dispose` and the modules' disposers run, and what it left running is taken away (see
1586
+ // below); the page's HTML is put back as the new version has it (Lamp's own elements stay), every module of the game
1587
+ // runs again in order (loaded from its source with its imports pointed at the new versions, as a swap loads one), and
1588
+ // the module that starts the game calls game() again, whose init starts from the snapshot taken before (read with
1589
+ // pendingRestore(), or given to restore()). What stays is what a reload loses: the WebGL or WebGPU context and the
1590
+ // shaders compiled for it, the files the runtime loaded, the sound, the controllers, the phones that joined, and every
1591
+ // part the game kept.
1592
+ //
1593
+ // A part is kept with `hot(import.meta).keep(key, build, { inputs, from, dispose, background })`: the first time it calls build()
1594
+ // and returns what that made (or its promise), and when the code runs again it returns the same thing without building
1595
+ // it, unless the module that kept it, or one it imports, changed since, the build function's own code changed, or one
1596
+ // of `inputs` isn't the same as before (another part's result it's built from, as a dependency list goes), or a file
1597
+ // it read as it was built (loaded through the runtime, asset(), texture(), model(), sound.load(), or fetched) changed.
1598
+ // The module that starts the game imports everything and changes often, so what it keeps goes by the modules whose
1599
+ // imports the build function names, and theirs: a change of its own code outside the build function keeps what it kept.
1600
+ // `from` declares what the part is built from beyond that: files beside the page, a module meaning it and what it
1601
+ // imports, a path ending in / a folder (`from: ['js/map-data.js', 'data/streets/']`). A change to one builds the part
1602
+ // again in place even where its module would take the change by a swap, since a swap leaves what was built as it was,
1603
+ // and the parts made from it (their `inputs`) with it; everything else stays, and so does the player's place. A part
1604
+ // that's built again, or not asked for any more, goes: `dispose(value)` when given, else a three.js object (or each in a
1605
+ // plain object, as { group }) is taken out of its scene and its geometry and materials let go, and anything else with
1606
+ // dispose() or destroy() has it called. A part that's slow to build says `background: true`, and the game plays on
1607
+ // while it's built again (see "Building in the background").
1608
+ //
1609
+ // const renderer = await hot(import.meta).keep('renderer', async () => { const r = new THREE.WebGPURenderer(); await r.init(); return r; });
1610
+ // const blocks = live.keep('blocks', () => buildBlocks(landmarks.colliders), { inputs: [landmarks] });
1611
+ // const paint = live.keep('roadPaint', () => buildRoadPaint(streets.network), { inputs: [streets], from: ['js/map-data.js'] });
1612
+ //
1613
+ // After a run, Lamp is told what was built again (each part, how long it took and why) and what of the change nothing
1614
+ // read again (a file the game uses whose part was kept, or that its code read once at the start): that waits for the
1615
+ // person's Reload, which opens the page again, and Lamp's chat says so rather than that it's in. The first frame of the
1616
+ // new code has its shaders compiled before it's drawn, with the last frame held on screen, and the report says how long
1617
+ // the game stood still for the change, from the old code's last frame to the new code's first.
1618
+ //
1619
+ // What the game's code leaves running is watched from the start: listeners it adds (to the window, the document or
1620
+ // anything else), timers, sound contexts it makes itself and workers. Each is taken away when the code runs again,
1621
+ // unless a part kept it (made while a keep's build ran). A module's own state starts again with it, as on a reload.
1622
+ // Running again isn't tried where the import map or a script that isn't a module changed, Lamp's own files changed,
1623
+ // modules import each other in a circle, or the page has no module scripts: the change waits for the person's Reload
1624
+ // (`reopenOrHold`), which opens the page again from the snapshot. New code that fails as it runs again, before init is
1625
+ // done or in its first second, opens the page again at once, from the same snapshot. The renderer, the cameras and the
1626
+ // game's state are registered rather than kept this way (see "What the game registers").
1627
+
1628
+ let runningAgain = null, loops = 0, fitting = false, pageText = null, watching = false, reopenedFrame = 0;
1629
+ // What the game kept, by the module and key it was kept with.
1630
+ const kept = new Map();
1631
+ // Above 0 while a part's build function runs: what it starts belongs to the part, and stays.
1632
+ let keeping = 0;
1633
+
1634
+ function keep(path, key, build, options = {}) {
1635
+ const { dispose, inputs = [], lasting = false, from = [], background = false } = typeof options === 'function' ? { dispose: options } : options;
1636
+ const id = `${path} ${key}`, text = lasting ? bare(source(build)) : source(build), had = kept.get(id), again = runningAgain;
1637
+ if (lasting && had && had.text !== text && redeclare(key)) return had.value;
1638
+ const sameAs = list => list.length === inputs.length && inputs.every((v, i) => Object.is(v, list[i]));
1639
+ const same = had && sameAs(had.inputs);
1640
+ const sources = [from].flat().filter(f => typeof f === 'string' && f).map(clean);
1641
+ const sameSources = had && had.from.join('\n') === sources.join('\n');
1642
+ // A part being built in the background goes on being built while nothing it's built from changed again, and the
1643
+ // game goes on with the part it has until the new one swaps in.
1644
+ if (had && had.pending) {
1645
+ const p = had.pending;
1646
+ if (!had.stale && p.text === text && sameAs(p.inputs) && p.from.join('\n') === sources.join('\n')) {
1647
+ had.used = true;
1648
+ if (again) tally.building.push({ key, cause: p.cause });
1649
+ return had.value;
1650
+ }
1651
+ stopBuilding(had);
1652
+ }
1653
+ if (had && !had.stale && had.text === text && same && sameSources) {
1654
+ had.used = true;
1655
+ // Built in the background since the code last ran: in the game from this run on.
1656
+ if (again && had.fresh) { had.fresh = false; tally.swapped.push(had); }
1657
+ else if (again) tally.kept.push(key);
1658
+ // The random numbers its build drew are drawn past, so what comes after gets the numbers it got then.
1659
+ for (let i = 0; i < (had.draws || 0); i++) random();
1660
+ return had.value;
1661
+ }
1662
+ // Why it's built again, for the agent: the file that changed, its own code, or a part it's built from.
1663
+ const cause = !had ? null : typeof had.stale === 'string' ? had.stale : had.text !== text ? 'its build changed'
1664
+ : !same ? 'a part it\'s built from was built again' : !sameSources ? 'what it\'s built from changed' : null;
1665
+ // Built from a part that's being built in the background: built from the new one once that swaps in, and until
1666
+ // then the game goes on with the one it has.
1667
+ // (an async part's value is its promise; what the game passes on is what it gave, `made`)
1668
+ if (again && had && !lasting && inputs.some(v => [...kept.values()].some(k => k.pending && (Object.is(k.value, v) || Object.is(k.made, v))))) {
1669
+ had.used = true;
1670
+ tally.waiting.push(key);
1671
+ return had.value;
1672
+ }
1673
+ // A part built in the background is built again while the game goes on with the one it has (see "Building in the
1674
+ // background").
1675
+ if (background && again && had && !lasting) {
1676
+ had.used = true;
1677
+ had.stale = false;
1678
+ had.pending = { text, inputs, from: sources, dispose: typeof dispose === 'function' ? dispose : null, build, cause,
1679
+ gen: again.change.gen, began: performance.now(), busy: 0, steps: null, assets: new Set(), timers: [] };
1680
+ backgrounds.add(had);
1681
+ tally.building.push({ key, cause });
1682
+ slices();
1683
+ return had.value;
1684
+ }
1685
+ const entry = { path, key, text, inputs, lasting, from: sources, dispose: typeof dispose === 'function' ? dispose : null, used: true,
1686
+ stale: false, value: undefined, made: undefined, assets: new Set(), draws: 0, cause, ms: null };
1687
+ const began = performance.now();
1688
+ // What the build starts as it runs belongs to the part. An async build's later work can't be told from other code
1689
+ // that runs while it waits, so of what's started meanwhile only what's the part's own stays with it: listeners on
1690
+ // the part (its object, its canvas, an element inside it) and a worker or sound context that is the part or one of
1691
+ // its values. The rest is watched as the game's; a timer started while it waits can't be told apart, so a part that
1692
+ // started one still running is built again when the code next runs (a part keeps a timer it starts before its first
1693
+ // await).
1694
+ keeping++;
1695
+ building.push(entry);
1696
+ const drawn = draws;
1697
+ let value;
1698
+ // A build made of steps (a generator) is built whole the first time: there's no part yet to play with meanwhile.
1699
+ try { value = build(); if (background && isSteps(value)) value = stepThrough(value); } finally { keeping--; building.pop(); entry.draws = draws - drawn; }
1700
+ if (value && typeof value.then === 'function') {
1701
+ const waiting = { added: [], made: [], timers: [], assets: entry.assets };
1702
+ keepsWaiting.add(waiting);
1703
+ value = Promise.resolve(value).finally(() => keepsWaiting.delete(waiting));
1704
+ const drawnBefore = draws;
1705
+ value.then(v => { entry.made = v; entry.settled = true; entry.timers = waiting.timers; entry.ms = performance.now() - began; entry.span = [began, began + entry.ms]; if (draws !== drawnBefore) entry.draws = null; own(v, waiting); },
1706
+ () => { entry.settled = true; if (kept.get(id) === entry) kept.delete(id); });
1707
+ } else { entry.made = value; entry.settled = true; entry.ms = performance.now() - began; entry.span = [began, began + entry.ms]; }
1708
+ entry.value = value;
1709
+ // The part it replaces goes, sparing what the new one shares with it (materials made once, so compiled once).
1710
+ if (had) Promise.resolve(value).then(v => letGo(had, usedBy(v)), () => letGo(had));
1711
+ kept.set(id, entry);
1712
+ if (again && had) tally.built.push(entry);
1713
+ return value;
1714
+ }
1715
+
1716
+ // The kept parts built from one of `paths`, as the game declared them (`from`: a file, a module and what it imports, or
1717
+ // a folder ending in /): a change to one is the part's own, so it's built again in place rather than swapped around it.
1718
+ function builtFrom(paths) {
1719
+ for (const k of kept.values()) {
1720
+ if (k.lasting || !k.from.length) continue;
1721
+ const deps = new Set();
1722
+ for (const f of k.from) if (!f.endsWith('/')) { deps.add(f); for (const dep of below(f)) deps.add(dep); }
1723
+ const hit = paths.find(p => deps.has(p) || k.from.some(f => f.endsWith('/') && p.startsWith(f)));
1724
+ if (hit) return { key: k.key, path: hit };
1725
+ }
1726
+ return null;
1727
+ }
1728
+
1729
+ const isKept = v => [...kept.values()].some(k => k.made === v);
1730
+
1731
+ // Async builds still running, each with the listeners added while it ran.
1732
+ const keepsWaiting = new Set();
1733
+ // The parts whose build is running now, and the files the runtime loaded for each (`took`): a change to one of those
1734
+ // files builds the part again.
1735
+ const building = [];
1736
+ function took(path) {
1737
+ loaded.add(path);
1738
+ read(path);
1739
+ }
1740
+ // A file of the page's read, through the runtime or fetched: by the parts being built, and since the code last ran again
1741
+ // (`readSince`, for what of a change nothing read again: `waitsFor`).
1742
+ let readSince = new Set();
1743
+ function read(path) {
1744
+ readSince.add(path);
1745
+ everRead.add(path);
1746
+ for (const entry of building) entry.assets.add(path);
1747
+ for (const waiting of keepsWaiting) waiting.assets.add(path);
1748
+ }
1749
+ // The parts' timer lists a timer's callback runs for, while it runs.
1750
+ let timedFor = null;
1751
+ function own(made, waiting) {
1752
+ const parts = [made, made && made.domElement, made && made.canvas].filter(o => o && typeof o === 'object');
1753
+ const of = target => parts.some(p => p === target || (p.contains && target.nodeType && p.contains(target)));
1754
+ const values = made && typeof made === 'object' ? [made, ...Object.values(made)] : [made];
1755
+ for (const [thing, end] of waiting.made) if (values.includes(thing)) owned.delete(end);
1756
+ for (const [target, listener] of waiting.added) {
1757
+ if (!of(target)) continue;
1758
+ const list = listenersOf.get(target), i = list ? list.indexOf(listener) : -1;
1759
+ if (i >= 0) list.splice(i, 1);
1760
+ }
1761
+ }
1762
+
1763
+ // `spare` holds the geometry and materials a part that replaces this one goes on using (made once and shared by both,
1764
+ // so the new part's shaders were compiled long ago): those stay, and so does whatever the game still holds elsewhere
1765
+ // (`stillHeld`). A part with a `dispose` of its own takes care of everything it made, and one with no three.js objects
1766
+ // has nothing to look for. What the game's listeners throw costs only the object they were told of.
1767
+ function letGo(k, spare = new Set()) {
1768
+ // A part built in the background lets go of the one it replaced once it's let go of itself.
1769
+ if (k.replaces) { letGo(k.replaces); k.replaces = null; }
1770
+ Promise.resolve(k.value).then(v => {
1771
+ try {
1772
+ if (k.dispose) return k.dispose(v);
1773
+ const failed = e => log(`Lamp: letting go of ${k.key} kept in ${k.path} failed (${e && e.message || e}).`);
1774
+ const parts = objectsOf(v);
1775
+ if (parts.length) {
1776
+ // A part that's a plain object of three.js objects ({ group, heights }) lets go of each, out of its scene
1777
+ // first, or the scene would count as still holding it.
1778
+ const roots = keptRoots(), objects = parts.filter(o => !roots.has(o));
1779
+ for (const o of objects) { try { o.removeFromParent(); } catch (e) { failed(e); } }
1780
+ const held = stillHeld(roots);
1781
+ for (const o of objects) {
1782
+ // A scene still being drawn waits: disposed of now, the renderer would only make it all again, and
1783
+ // nothing would dispose of that. It's disposed of when it's taken apart.
1784
+ if (held.has(o)) { retire(o, true); continue; }
1785
+ disposeTree(o, spare, held, failed);
1786
+ retire(o);
1787
+ }
1788
+ }
1789
+ if (v && !v.isObject3D && typeof v.dispose === 'function') v.dispose();
1790
+ else if (v && !v.isObject3D && typeof v.destroy === 'function') v.destroy(true);
1791
+ } catch (e) { log(`Lamp: letting go of ${k.key} kept in ${k.path} failed (${e && e.message || e}).`); }
1792
+ }, () => {});
1793
+ }
1794
+
1795
+ // A part let go of can go on being reachable: a module runs again as a new module, and the browser keeps the one it
1796
+ // replaces, with whatever its variables point at, for as long as the page is open, and a kept renderer holds on to
1797
+ // what it drew. Left as they are, a few such references keep every old part's buffers and objects in the page, the
1798
+ // garbage collector walks them all, and each run of the code takes longer than the last until the page runs out of
1799
+ // memory. So once nothing is deciding what the game goes on with, what's left of a part is taken apart: its objects
1800
+ // lose their children and their own hold on geometry and materials, and whatever still points at them points at
1801
+ // nothing heavy. Objects stay valid, on blank geometry and a blank material, for old code that's still calling them.
1802
+ //
1803
+ // Only roots are remembered, weakly: a part nothing holds simply goes, and this list keeps nothing alive.
1804
+ const retired = [], blanks = new Map();
1805
+ function disposeTree(root, spare, held, failed = e => { throw e; }) {
1806
+ const dispose = thing => { if (spare.has(thing) || held.has(thing)) return; try { thing.dispose(); } catch (e) { failed(e); } };
1807
+ root.traverse(o => {
1808
+ if (o.geometry) dispose(o.geometry);
1809
+ for (const m of [o.material].flat()) if (m && m.dispose) dispose(m);
1810
+ });
1811
+ }
1812
+ let emptying = false, emptyAfter = 3000;
1813
+ function retire(o, undisposed = false) {
1814
+ retired.push({ ref: new WeakRef(o), undisposed });
1815
+ emptyAfter = 3000;
1816
+ emptySoon();
1817
+ }
1818
+ function emptySoon() {
1819
+ if (emptying) return;
1820
+ emptying = true;
1821
+ after(emptyAfter, emptyRetired);
1822
+ }
1823
+ function emptyRetired() {
1824
+ emptying = false;
1825
+ if (!retired.length) return;
1826
+ // A run under way, a build still to finish or an async part still to arrive may yet use what's in the list.
1827
+ if (runningAgain || backgrounds.size || [...kept.values()].some(k => !k.settled)) return emptySoon();
1828
+ try {
1829
+ const roots = keptRoots(), held = stillHeld(roots), drawing = scenesDrawing(), list = retired.splice(0);
1830
+ for (const entry of list) {
1831
+ const root = entry.ref.deref();
1832
+ if (!root || root.parent || roots.has(root)) continue;
1833
+ // Put back in a scene, or kept by something else since: no longer retired. A scene something still draws
1834
+ // is asked about again, more rarely each time, and goes once it isn't drawn.
1835
+ if (drawing.includes(root)) { retired.push(entry); continue; }
1836
+ if (held.has(root)) continue;
1837
+ if (entry.undisposed) disposeTree(root, new Set(), held);
1838
+ emptyOut(root, held);
1839
+ }
1840
+ // Only ever a saving: what couldn't be taken apart goes on as it was.
1841
+ } catch (e) { log(`Lamp: taking apart what the game let go of failed (${e && e.message || e}).`); }
1842
+ if (!retired.length) { emptyAfter = 3000; return; }
1843
+ emptyAfter = Math.min(emptyAfter * 2, 60000);
1844
+ emptySoon();
1845
+ }
1846
+ function emptyOut(root, held) {
1847
+ const stack = [root];
1848
+ while (stack.length) {
1849
+ const o = stack.pop();
1850
+ if (held.has(o)) continue;
1851
+ // A child that's held elsewhere leaves too: its parent no longer lists it, so it can't go on inheriting from it.
1852
+ for (const child of o.children) { if (child.parent === o) child.parent = null; stack.push(child); }
1853
+ o.children = [];
1854
+ if (o.geometry) o.geometry = blankOf(o.geometry);
1855
+ if (o.material) o.material = blankOf([o.material].flat()[0]);
1856
+ }
1857
+ }
1858
+ // One empty object of the plainest class a geometry or material comes from, beneath three.js's event dispatcher.
1859
+ function blankOf(thing) {
1860
+ if (!thing) return null;
1861
+ const chain = [];
1862
+ for (let c = thing.constructor; c && c !== Function.prototype; c = Object.getPrototypeOf(c)) chain.push(c);
1863
+ const base = chain[Math.max(0, chain.length - 2)];
1864
+ if (!blanks.has(base)) { let made = null; try { made = new base(); } catch (e) {} blanks.set(base, made); }
1865
+ return blanks.get(base);
1866
+ }
1867
+ // What the game keeps by name: the three.js objects, geometry and materials of every part kept (with its pools,
1868
+ // however they're held, so a detached object counts), of what's being built or waiting to be swapped in, and the
1869
+ // cameras registered. `stillHeld` is all that with what hangs under it, and every scene the renderer is drawing.
1870
+ function keptRoots() {
1871
+ const roots = new Set(), seen = new Set();
1872
+ const walk = (v, depth) => {
1873
+ if (!v || typeof v !== 'object') return;
1874
+ if (v.isObject3D || v.isBufferGeometry || v.isMaterial) { roots.add(v); return; }
1875
+ // What's kept can be big tables: a row at the last depth has been dealt with by here.
1876
+ if (depth <= 0 || seen.has(v) || ArrayBuffer.isView(v) || v instanceof ArrayBuffer || (globalThis.Node && v instanceof Node)) return;
1877
+ seen.add(v);
1878
+ // Another library's object can throw on being read: what can't be read can't be told to be held.
1879
+ let each = [];
1880
+ try { each = v instanceof Map ? [...v.values()] : v instanceof Set ? [...v] : Object.values(v); } catch (e) {}
1881
+ for (const x of each) walk(x, depth - 1);
1882
+ };
1883
+ for (const k of kept.values()) {
1884
+ walk(k.made, 3);
1885
+ if (k.replaces) walk(k.replaces.made, 3);
1886
+ if (k.pending) walk(k.pending.value, 3);
1887
+ }
1888
+ for (const entry of cameras.values()) walk(entry, 2);
1889
+ return roots;
1890
+ }
1891
+ function stillHeld(roots = keptRoots()) {
1892
+ const held = new Set();
1893
+ const add = root => root.traverse(o => {
1894
+ held.add(o);
1895
+ if (o.geometry) held.add(o.geometry);
1896
+ for (const x of [o.material].flat()) if (x) held.add(x);
1897
+ });
1898
+ for (const root of roots) { if (root.isObject3D) add(root); else held.add(root); }
1899
+ for (const scene of scenesDrawing()) if (!held.has(scene)) add(scene);
1900
+ return held;
1901
+ }
1902
+
1903
+ // MARK: Building in the background
1904
+ //
1905
+ // A part that takes long to build (a city from its map data, a terrain, a navigation mesh) says so with
1906
+ // `background: true`, and when a change builds it again the game goes on as it was while the new one is built: frames
1907
+ // keep coming, the controller and the game's logic keep working with the part it has, and a small "Building…" shows
1908
+ // over the game. Once the new part is built, its materials' shaders are compiled while the old one is still drawn, and
1909
+ // the code runs again in place with it, as for any change, which takes about a frame since everything else is kept:
1910
+ // what used the old part uses the new one from there, and the old one goes. A part built from one being built (its
1911
+ // `inputs`) is built again from the new one once that's in, and parts that must change together are built in one
1912
+ // part. The first build, with no part to play meanwhile, is built whole as the game starts.
1913
+ //
1914
+ // Its build is made of steps or waits for its work done elsewhere:
1915
+ //
1916
+ // - a generator that yields between steps (a row of the city, a chunk of the terrain), which the runtime steps a few
1917
+ // milliseconds each frame;
1918
+ // - or an async function that hands the pure data work (parsing, geometry arrays) to a worker with work(), making
1919
+ // only the three.js objects itself from the arrays it gets back;
1920
+ // - a plain function is built in one go a frame after the code ran again: it still holds that frame, but nothing
1921
+ // else does.
1922
+ //
1923
+ // const city = live.keep('city', function* () {
1924
+ // const rows = [];
1925
+ // for (const block of MAP.blocks) { rows.push(buildBlock(block)); yield; }
1926
+ // return { group: merge(rows) };
1927
+ // }, { from: ['js/map-data.js'], background: true });
1928
+ //
1929
+ // The part's three.js objects are compiled for the scene the game last drew, with the camera it drew it with, when the
1930
+ // part is one or a plain object holding them ({ group }), a material a frame. A material made anew still holds the frames
1931
+ // on screen while its shader compiles, so a part's materials are made once, in a kept part of their own, and the part
1932
+ // built in the background makes only geometry with them: the old part goes without the materials the new one uses. The log and Lamp's report say what was built in the
1933
+ // background and for how long, and how long the game stood still for the change (about a frame).
1934
+
1935
+ // The parts being built in the background: each the part the game has now, with what's being built in `pending`.
1936
+ const backgrounds = new Set();
1937
+ // Each frame, the steps of what's built in the background get this many milliseconds, fewer when the game's own
1938
+ // frame is slow, so the game keeps its frame rate as it's built.
1939
+ let slicing = false, frameCost = 0;
1940
+ const isSteps = v => !!v && typeof v.next === 'function' && typeof v[Symbol.iterator] === 'function' && typeof v.then !== 'function';
1941
+ function stepThrough(steps) {
1942
+ for (;;) { const s = steps.next(); if (s.done) return s.value; }
1943
+ }
1944
+ function slices() {
1945
+ if (slicing || !backgrounds.size) return;
1946
+ slicing = true;
1947
+ after(400, () => { if (backgrounds.size) showHint(); });
1948
+ // Between frames, or every 50 ms where frames come slower (a window out of sight), so the build goes on.
1949
+ const mine = ++sliceCount, go = () => { if (mine === sliceCount) { sliceCount++; slice(); } };
1950
+ nextFrame(go);
1951
+ after(50, go);
1952
+ }
1953
+ let sliceCount = 0;
1954
+ function slice() {
1955
+ const began = performance.now(), budget = Math.max(4, Math.min(10, 14 - frameCost));
1956
+ for (const k of [...backgrounds]) {
1957
+ while (k.pending && !k.pending.waits && performance.now() - began < budget) stepPart(k);
1958
+ }
1959
+ slicing = false;
1960
+ if ([...backgrounds].some(k => k.pending && !k.pending.waits)) slices();
1961
+ }
1962
+ function stepPart(k) {
1963
+ const p = k.pending, t = performance.now();
1964
+ keeping++;
1965
+ building.push(p);
1966
+ try {
1967
+ if (!p.steps) {
1968
+ const made = p.build();
1969
+ if (isSteps(made)) { p.steps = made; return; }
1970
+ if (made && typeof made.then === 'function') {
1971
+ // Its work is done elsewhere (a worker): what it starts meanwhile is the part's when it's the part's own.
1972
+ p.waits = true;
1973
+ const waiting = { added: [], made: [], timers: p.timers, assets: p.assets };
1974
+ keepsWaiting.add(waiting);
1975
+ Promise.resolve(made).finally(() => keepsWaiting.delete(waiting))
1976
+ .then(v => { own(v, waiting); built(k, p, v); }, e => failed(k, p, e));
1977
+ return;
1978
+ }
1979
+ return built(k, p, made);
1980
+ }
1981
+ const s = p.steps.next();
1982
+ if (s.done) built(k, p, s.value);
1983
+ } catch (e) { failed(k, p, e); }
1984
+ finally { keeping--; building.pop(); p.busy += performance.now() - t; }
1985
+ }
1986
+ // Whether a part being built in the background is built from `path`: what changed it, what its last build read, or
1987
+ // what it declares it's built from. Its new build reads it, so it doesn't wait for Reload.
1988
+ function buildingFrom(path) {
1989
+ for (const k of backgrounds) {
1990
+ if (!k.pending) continue;
1991
+ if (k.pending.cause === path || k.assets.has(path) || k.pending.assets.has(path)) return true;
1992
+ if (k.from.some(f => f === path || (f.endsWith('/') && path.startsWith(f)) || (!f.endsWith('/') && below(f).has(path)))) return true;
1993
+ }
1994
+ return false;
1995
+ }
1996
+
1997
+ // Built: its shaders are compiled while the old part is drawn, and it waits to be swapped in with the others.
1998
+ async function built(k, p, value) {
1999
+ p.waits = true;
2000
+ p.value = value;
2001
+ p.ms = performance.now() - p.began;
2002
+ p.compiled = await compileAhead(value);
2003
+ // Not while the code runs again: that run decides which part the game goes on with.
2004
+ while (runningAgain) await new Promise(done => after(50, done));
2005
+ // Built again since, or let go of: what this build made goes.
2006
+ if (k.pending !== p) {
2007
+ // Sparing what the parts kept still use: the materials made once and shared, the geometry of the part on screen.
2008
+ const spare = new Set();
2009
+ for (const other of kept.values()) for (const used of usedBy(other.made)) spare.add(used);
2010
+ return letGo({ key: k.key, path: k.path, value, dispose: p.dispose }, spare);
2011
+ }
2012
+ k.pending = null;
2013
+ backgrounds.delete(k);
2014
+ const entry = { path: k.path, key: k.key, text: p.text, inputs: p.inputs, lasting: false, from: p.from, dispose: p.dispose, used: false,
2015
+ stale: false, value, made: value, assets: p.assets, timers: p.timers, draws: 0, cause: p.cause, ms: p.ms,
2016
+ busy: p.busy, compiled: p.compiled, gen: p.gen, fresh: true, replaces: k, settled: true };
2017
+ kept.set(`${k.path} ${k.key}`, entry);
2018
+ log(`Lamp: ${k.key} was built in the background in ${seconds(p.ms)} while the game ran${p.compiled == null ? '' : ` (its shaders compiled ahead in ${Math.round(p.compiled)} ms)`}.`);
2019
+ if (!backgrounds.size) swapIn();
2020
+ }
2021
+ function failed(k, p, e) {
2022
+ if (k.pending !== p) return;
2023
+ k.pending = null;
2024
+ backgrounds.delete(k);
2025
+ if (p.steps) { try { p.steps.return(); } catch (e2) {} }
2026
+ const why = `building ${k.key} in the background failed (${e && e.message || e}), so the game goes on with the one it had`;
2027
+ log(`Error: ${why}.`);
2028
+ // Lamp hears it's no longer being built, and the agent why.
2029
+ tellHost(p.gen, 'report', why, { built: [{ part: k.key, background: true, ms: Math.round(performance.now() - p.began), cause: 'its build failed' }], waits: [], later: true });
2030
+ if (!backgrounds.size) { hideHint(); swapIn(); }
2031
+ }
2032
+ function stopBuilding(k) {
2033
+ const p = k.pending;
2034
+ k.pending = null;
2035
+ backgrounds.delete(k);
2036
+ if (p && p.steps) { try { p.steps.return(); } catch (e) {} }
2037
+ if (!backgrounds.size) hideHint();
2038
+ }
2039
+ const seconds = ms => ms < 10000 ? `${(ms / 1000).toFixed(1)} s` : `${Math.round(ms / 1000)} s`;
2040
+
2041
+ // What the game drew last (the first render of the frame), for compiling a part ahead for that scene and camera; and a
2042
+ // compile under way before the first frame after the code ran again (`warming`).
2043
+ let lastDrawn = null, drewThisFrame = false, warming = null, warmed = null;
2044
+ // Watches the renderer's render() for what it draws, and after the code ran again has its first frame's shaders
2045
+ // compiled with compileAsync() before it's drawn, the old frame held on screen meanwhile, so nothing compiles in a
2046
+ // frame the player sees. A renderer without compileAsync (Phaser's) is left as it is.
2047
+ // Every scene drawn lately, weakly, with the frame it was last drawn in: a second scene (a minimap's, a mirror's) is the
2048
+ // game's as much as the first, and one not drawn for a while no longer is. Counted in frames, so a run of the code or a
2049
+ // window out of sight, which draw nothing, don't make a scene look abandoned.
2050
+ const drawnScenes = [];
2051
+ let framesDrawn = 0;
2052
+ function noteDrawn(scene) {
2053
+ if (!scene || !scene.isObject3D) return;
2054
+ const known = drawnScenes.find(d => d.ref.deref() === scene);
2055
+ if (known) { known.frame = framesDrawn; return; }
2056
+ drawnScenes.push({ ref: new WeakRef(scene), frame: framesDrawn });
2057
+ if (drawnScenes.length > 8) drawnScenes.splice(drawnScenes.indexOf(drawnScenes.reduce((a, b) => b.frame < a.frame ? b : a)), 1);
2058
+ }
2059
+ function scenesDrawing() {
2060
+ return drawnScenes.filter(d => framesDrawn - d.frame <= 60).map(d => d.ref.deref()).filter(Boolean);
2061
+ }
2062
+ function watchRender(r) {
2063
+ if (!r || r.__lampRender || typeof r.render !== 'function' || typeof r.compileAsync !== 'function') return;
2064
+ const draw = r.render;
2065
+ r.__lampRender = true;
2066
+ r.render = function (scene, camera, ...rest) {
2067
+ if (!drewThisFrame && scene && scene.isObject3D && camera && camera.isCamera) { lastDrawn = { scene, camera }; drewThisFrame = true; }
2068
+ noteDrawn(scene);
2069
+ if (warming === 'ask' && lastDrawn) {
2070
+ const t = performance.now();
2071
+ warming = Promise.resolve().then(() => this.compileAsync(scene, camera)).catch(() => {})
2072
+ .then(() => { warmed = performance.now() - t; warming = null; });
2073
+ return;
2074
+ }
2075
+ if (warming) return;
2076
+ return draw.call(this, scene, camera, ...rest);
2077
+ };
2078
+ }
2079
+ // A part's three.js objects compiled ahead for the scene and camera the game draws, a material at a time with a frame
2080
+ // between: a pipeline's compiling holds the frames the GPU presents meanwhile (WebKit's WebGPU), so one at a time keeps
2081
+ // each hold to one material's.
2082
+ const objectsOf = v => v && v.isObject3D ? [v] : isPlain(v) ? Object.values(v).filter(o => o && o.isObject3D) : [];
2083
+ // The geometry and materials of a part's three.js objects.
2084
+ function usedBy(v) {
2085
+ const used = new Set();
2086
+ for (const o of objectsOf(v)) o.traverse(m => { if (m.geometry) used.add(m.geometry); for (const x of [m.material].flat()) if (x) used.add(x); });
2087
+ return used;
2088
+ }
2089
+ async function compileAhead(value) {
2090
+ const objects = objectsOf(value);
2091
+ if (!objects.length || !lastDrawn || !renderer || typeof renderer.compileAsync !== 'function') return null;
2092
+ const t = performance.now(), seen = new Set(), each = [];
2093
+ for (const o of objects) o.traverse(m => {
2094
+ const materials = [m.material].flat().filter(Boolean);
2095
+ if (!materials.length || materials.every(x => seen.has(x))) return;
2096
+ materials.forEach(x => seen.add(x));
2097
+ each.push(m);
2098
+ });
2099
+ for (const m of each) {
2100
+ try { await renderer.compileAsync(m, lastDrawn.camera, lastDrawn.scene); } catch (e) {}
2101
+ await new Promise(nextFrame);
2102
+ }
2103
+ return performance.now() - t;
2104
+ }
2105
+
2106
+ // The parts built in the background go in: the code runs again in place, at once, or at the next pause while the game
2107
+ // is busy. A change that ran the code again meanwhile has already put them in.
2108
+ let swapWaits = false;
2109
+ function swapIn() {
2110
+ queue = queue.then(() => {
2111
+ const fresh = [...kept.values()].filter(k => k.fresh);
2112
+ if (!fresh.length || !frameFunction || runningAgain) return;
2113
+ if (isBusy()) { swapWaits = true; return; }
2114
+ hideHint();
2115
+ const gen = Math.max(...fresh.map(k => k.gen || 0));
2116
+ return runAgain({ gen, why: `${fresh.map(k => k.key).join(', ')} built in the background`, changed: [], quiet: true }, takeSnapshot(), phonesHere());
2117
+ }).catch(e => log(`Lamp: ${e && e.message || e}`));
2118
+ }
2119
+
2120
+ // "Building…" over the game while a part is built in the background, at the top right, below where the app around
2121
+ // the page puts its own buttons.
2122
+ let hint = null;
2123
+ function showHint() {
2124
+ if (!document.body) return;
2125
+ if (!hint) {
2126
+ hint = document.createElement('div');
2127
+ hint.textContent = 'Building…';
2128
+ hint.style.cssText = 'position:fixed;z-index:20;right:14px;top:calc(48px + env(safe-area-inset-top));pointer-events:none;'
2129
+ + 'padding:6px 14px;border-radius:16px;background:rgba(20,20,24,.6);color:rgba(255,255,255,.85);font:600 13px -apple-system,system-ui,sans-serif;'
2130
+ + '-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);transition:opacity .3s;opacity:0';
2131
+ }
2132
+ if (!hint.isConnected) document.body.append(hint);
2133
+ nextFrame(() => { if (hint) hint.style.opacity = '1'; });
2134
+ }
2135
+ function hideHint() { if (hint) hint.style.opacity = '0'; }
2136
+
2137
+ // MARK: What the game registers
2138
+ //
2139
+ // A part kept with keep() stays while its code does. What the game registers stays whatever else changes, because the
2140
+ // runtime holds it by name rather than the game's code holding it, so a game that registers these keeps its state
2141
+ // through every change that doesn't change the thing itself, the page opening again included:
2142
+ //
2143
+ // const live = hot(import.meta);
2144
+ // const renderer = live.renderer(() => new THREE.WebGLRenderer({ antialias: true }));
2145
+ // const camera = live.camera('main', () => new THREE.PerspectiveCamera(60, 16 / 9, 0.1, 500), { target: () => controls.target });
2146
+ // const play = live.state('play', { version: 1, start: () => ({ mode: 'title', level: 1, score: 0, player: { x: 0, z: 0 } }) });
2147
+ //
2148
+ // `renderer(build)`: there's one renderer. build() runs only when there isn't one yet: new code gets the renderer the
2149
+ // game has before it could make a second, with its context and compiled shaders, and a change to any module leaves it
2150
+ // alone. It's made again only when build's own code changes. An async build (`WebGPURenderer` and its init()) gives a
2151
+ // promise: `await` it. Pass the result to game().
2152
+ //
2153
+ // `camera(id, build, { target })`: the camera build() makes stays, and where it is, how it's turned and what lens it has
2154
+ // are put back before the first frame whatever the new start set, also when the page opens again or a new install does.
2155
+ // So build() sets its starting pose and lens. `target()` gives something that has x, y and z the camera looks at and new
2156
+ // code makes again (OrbitControls' `target`): it's read as the old code stops and set as the new code starts. A change
2157
+ // to build's own code makes a new camera, at the old pose and with the new lens.
2158
+ //
2159
+ // `state(id, { version, start, migrate })`: `start()` gives a plain object, which is the game's to change as it plays and
2160
+ // never to replace. Lamp snapshots it, as plain JSON, and before init the same object holds what it held: fields the new
2161
+ // `start()` adds take their start values and every other keeps its value, nested plain objects field by field. So read
2162
+ // it where it's used, build the start from it (`play.mode` says what to show, with no title drawn first) and keep
2163
+ // nothing derived from it in a kept part. Raise `version` when a field's meaning changes: `migrate(old, oldVersion)`
2164
+ // gives the new state, and a version raised with no migrate, or one that throws, starts that state from `start()`. At a
2165
+ // fresh start it's `start()`. It must be registered once per run, before game()
2166
+ // starts or in init; read in a module's top level it still has its start values. A swap leaves what's registered as it
2167
+ // is, so a change to a declaration in a module that takes its changes runs the code again.
2168
+ //
2169
+ // This holds only for what's registered. A renderer or camera the game's code makes for itself, or the state it keeps
2170
+ // in its own variables, starts again with the code. So a change that would run the code again of a game whose place
2171
+ // isn't kept (no state registered, and no snapshot() it reads back) waits for the person's Reload rather than starting
2172
+ // play over, and says what isn't registered, for the agent to register in the same turn. Each run tells Lamp what it
2173
+ // kept, built again and reset (`cameThrough`).
2174
+
2175
+ const cameras = new Map(), states = new Map();
2176
+ // What the run starting now was given back.
2177
+ let tally = { kept: [], built: [], reset: [], building: [], waiting: [], swapped: [] }, flatGame = false, asksPlace = false;
2178
+ // What the run starting now is to put back: the old run's `heldNow()`, or what the page kept for its opening.
2179
+ let restoring = null;
2180
+ // What a swap's new versions declared differently. A swap keeps what's registered as it is, so it can't take that in: the
2181
+ // code runs again, where the new declaration counts. True when a swap is under way to hear of it.
2182
+ let redeclared = null;
2183
+ const redeclare = what => {
2184
+ if (!loading.size || runningAgain) return false;
2185
+ redeclared = redeclared || what;
2186
+ return true;
2187
+ };
2188
+ const LENS = ['fov', 'aspect', 'near', 'far', 'zoom', 'focus', 'filmGauge', 'filmOffset', 'left', 'right', 'top', 'bottom'];
2189
+
2190
+ function camera(id, build, { target } = {}) {
2191
+ const text = bare(source(build)), again = runningAgain;
2192
+ let entry = cameras.get(id);
2193
+ if (entry && entry.used) {
2194
+ if (entry.text !== text) redeclare(`camera ${id}`);
2195
+ return entry.value;
2196
+ }
2197
+ if (!entry || entry.text !== text) {
2198
+ if (entry && again) tally.built.push({ key: `camera ${id}`, ms: null, cause: 'its build changed' });
2199
+ cameras.set(id, entry = { id, text, value: build() });
2200
+ } else if (again) tally.kept.push(`camera ${id}`);
2201
+ entry.target = target;
2202
+ entry.used = true;
2203
+ return entry.value;
2204
+ }
2205
+
2206
+ function poseOf({ value: c, target, text }) {
2207
+ const pose = { text, position: c.position.toArray(), quaternion: c.quaternion.toArray(), up: c.up.toArray(), lens: {} };
2208
+ for (const key of LENS) if (typeof c[key] === 'number') pose.lens[key] = c[key];
2209
+ const point = target && target();
2210
+ if (point) pose.target = [point.x, point.y, point.z];
2211
+ return pose;
2212
+ }
2213
+
2214
+ function putPose(entry, pose) {
2215
+ if (!pose) return;
2216
+ const c = entry.value;
2217
+ c.position.fromArray(pose.position);
2218
+ c.quaternion.fromArray(pose.quaternion);
2219
+ c.up.fromArray(pose.up);
2220
+ // A camera whose build changed has the lens the change gave it.
2221
+ if (pose.text === entry.text) Object.assign(c, pose.lens);
2222
+ const point = entry.target && entry.target();
2223
+ if (point && pose.target) [point.x, point.y, point.z] = pose.target;
2224
+ if (c.updateProjectionMatrix) c.updateProjectionMatrix();
2225
+ if (c.updateMatrixWorld) c.updateMatrixWorld();
2226
+ }
2227
+
2228
+ function state(id, { version = 1, start, migrate } = {}) {
2229
+ let entry = states.get(id);
2230
+ if (!entry) states.set(id, entry = { id, value: {} });
2231
+ const text = `${version} ${bare(source(start))} ${migrate ? bare(source(migrate)) : ''}`;
2232
+ if (entry.used) {
2233
+ if (entry.text !== text) redeclare(`state ${id}`);
2234
+ return entry.value;
2235
+ }
2236
+ Object.assign(entry, { version, start, migrate, text, used: true, settled: false });
2237
+ fill(entry.value, startOf(entry));
2238
+ if (restoring) settle(entry);
2239
+ return entry.value;
2240
+ }
2241
+
2242
+ function startOf(entry) {
2243
+ const value = entry.start();
2244
+ if (!isPlain(value)) throw new Error(`state('${entry.id}'): start() gives a plain object`);
2245
+ return value;
2246
+ }
2247
+
2248
+ // The same object throughout, for whoever already holds it.
2249
+ function fill(into, from) {
2250
+ for (const key of Object.keys(into)) delete into[key];
2251
+ Object.assign(into, from);
2252
+ }
2253
+
2254
+ const merged = (start, was) => {
2255
+ if (!isPlain(start) || !isPlain(was)) return was;
2256
+ const both = { ...start };
2257
+ for (const key of Object.keys(was)) both[key] = key in start ? merged(start[key], was[key]) : was[key];
2258
+ return both;
2259
+ };
2260
+
2261
+ // Takes what the old run left into a registered state. Called as it's registered, and for those registered before
2262
+ // game() found out where the page starts.
2263
+ function settle(entry) {
2264
+ const was = restoring && restoring.states[entry.id];
2265
+ if (entry.settled || !entry.used || !was) return;
2266
+ entry.settled = true;
2267
+ const lost = why => { log(`Lamp: state ${entry.id} started from start() (${why}).`); tally.reset.push(`state ${entry.id}`); };
2268
+ if (was.version === entry.version) {
2269
+ fill(entry.value, merged(startOf(entry), was.value));
2270
+ tally.kept.push(`state ${entry.id}`);
2271
+ } else if (was.version < entry.version && entry.migrate) {
2272
+ try {
2273
+ fill(entry.value, merged(startOf(entry), entry.migrate(was.value, was.version)));
2274
+ tally.kept.push(`state ${entry.id}`);
2275
+ } catch (e) { lost(`migrate failed: ${e && e.message || e}`); }
2276
+ } else lost(`version ${was.version} became ${entry.version}${entry.migrate ? '' : ' with no migrate'}`);
2277
+ }
2278
+
2279
+ // Whether the player's place outlasts the code: state registered, or a snapshot() the game reads back.
2280
+ const keepsPlace = () => states.size > 0 || (!!hooks.snapshot && (!!hooks.restore || asksPlace));
2281
+
2282
+ const madeCamera = () => cameras.size > 0 || [...kept.values()].some(k => k.made && k.made.isCamera);
2283
+
2284
+ // What isn't registered, in the words the agent is told it in. A 2D game's renderer and cameras are Phaser's, made again
2285
+ // from its state. A game that says nothing of its renderer is told of it only here: `isKept` also takes one kept by hand.
2286
+ function unregistered() {
2287
+ const left = [];
2288
+ if (!flatGame && !isKept(renderer)) left.push('its renderer (live.renderer)');
2289
+ if (!flatGame && !madeCamera()) left.push('its camera (live.camera)');
2290
+ if (!keepsPlace()) left.push('its gameplay state (live.state)');
2291
+ return left;
2292
+ }
2293
+
2294
+ // What came through a change, for the log and for the agent: kept, built again, reset, and what started again unasked
2295
+ // because it isn't registered.
2296
+ function cameThrough(waits = []) {
2297
+ const left = unregistered();
2298
+ const why = k => !k.cause ? '' : /^[^\s]+[./][^\s]+$/.test(k.cause) ? `, ${k.cause} changed` : `, ${k.cause}`;
2299
+ const built = tally.built.map(k => k.ms == null && !k.cause ? k.key : `${k.key} (${k.ms == null ? '' : `${Math.round(k.ms)} ms`}${k.ms == null ? why(k).slice(2) : why(k)})`);
2300
+ const swapped = tally.swapped.map(k => `${k.key} (built in ${seconds(k.ms)}${why(k)})`);
2301
+ return [tally.kept.length ? `kept ${tally.kept.join(', ')}` : '', built.length ? `built again ${built.join(', ')}` : '',
2302
+ tally.building.length ? `building in the background ${tally.building.map(k => k.key + why(k).replace(/^, /, ' (') + (k.cause ? ')' : '')).join(', ')}` : '',
2303
+ swapped.length ? `put in what was built in the background: ${swapped.join(', ')}` : '',
2304
+ tally.waiting.length ? `built again once what they're built from is in: ${tally.waiting.join(', ')}` : '',
2305
+ tally.reset.length ? `reset ${tally.reset.join(', ')}` : '', left.length ? `not registered, so started again: ${left.join(', ')}` : '',
2306
+ waits.length ? `waits for Reload: ${waits.join(', ')}, which nothing read again` : ''].filter(Boolean).join('; ');
2307
+ }
2308
+
2309
+ // Everything registered, as plain JSON: for the new run, or for a page that opens again.
2310
+ function heldNow() {
2311
+ if (!cameras.size && !states.size) return null;
2312
+ const held = { cameras: {}, states: {} };
2313
+ for (const [id, entry] of cameras) held.cameras[id] = poseOf(entry);
2314
+ for (const [id, entry] of states) {
2315
+ try { held.states[id] = { version: entry.version, value: JSON.parse(JSON.stringify(entry.value)) }; }
2316
+ catch (e) { log(`Lamp: state ${id} isn't plain JSON (${e && e.message || e}), so it isn't kept.`); }
2317
+ }
2318
+ return held;
2319
+ }
2320
+
2321
+ // What a module imports, all the way down.
2322
+ function below(path) {
2323
+ const all = new Set(), next = [path];
2324
+ while (next.length) for (const dep of imports.get(next.pop()) || []) if (!all.has(dep)) { all.add(dep); next.push(dep); }
2325
+ return all;
2326
+ }
2327
+
2328
+ // What was kept by a changed module, or by one it imports, is built again when the code next runs. The module that
2329
+ // starts the game imports everything and changes often, so what it kept goes by what the build function names: the
2330
+ // modules whose imports it uses, and theirs.
2331
+ function markStale(paths) {
2332
+ for (const k of kept.values()) {
2333
+ if (k.stale || k.lasting) continue;
2334
+ let deps;
2335
+ // A game bundled into one file (from TypeScript) imports nothing of its own: a change anywhere in it is its own.
2336
+ if (rootPaths.includes(k.path) && (bindings.get(k.path) || new Map()).size) {
2337
+ deps = new Set();
2338
+ for (const [name, from] of bindings.get(k.path) || []) {
2339
+ if (!new RegExp(`(^|[^\\w$.])${name.replace(/\$/g, '\\$')}\\b`).test(k.text)) continue;
2340
+ deps.add(from);
2341
+ for (const dep of below(from)) deps.add(dep);
2342
+ }
2343
+ } else { deps = below(k.path); deps.add(k.path); }
2344
+ for (const f of k.from) if (!f.endsWith('/')) { deps.add(f); for (const dep of below(f)) deps.add(dep); }
2345
+ const hit = paths.find(p => deps.has(p) || k.assets.has(p) || k.from.some(f => f.endsWith('/') && p.startsWith(f)));
2346
+ if (hit) k.stale = hit;
2347
+ }
2348
+ }
2349
+
2350
+ const pagePath = () => pathOf(location.href.split(/[?#]/)[0]) || 'index.html';
2351
+ // The page's HTML as this copy started, to tell what of it a change changed.
2352
+ function keepPage() {
2353
+ if (!watching) return;
2354
+ readText(pagePath()).then(t => { if (pageText == null) pageText = t; }, () => {});
2355
+ }
2356
+ const parse = text => new DOMParser().parseFromString(text, 'text/html');
2357
+ // What of a page can't change under modules that have run: the import map and the scripts that aren't modules.
2358
+ const fixed = page => JSON.stringify([...page.querySelectorAll('script')].filter(s => s.type !== 'module')
2359
+ .map(s => [s.type, s.getAttribute('src'), s.getAttribute('src') ? '' : s.textContent.trim()]));
2360
+
2361
+ // Why the code can't run again in place for a change, or null when it can.
2362
+ function canRunAgain(change) {
2363
+ if (!watching) return 'the game\'s code runs again in place only where Lamp puts changes into the open game';
2364
+ if (!frameFunction) return 'the game hadn\'t started';
2365
+ const own = (change.changed || []).map(clean).find(p => lampOwn(p));
2366
+ if (own) return `Lamp's own ${own} changed`;
2367
+ return null;
2368
+ }
2369
+
2370
+ // The game's modules that name a changed data file the runtime didn't load (one a module fetches itself, as a city's
2371
+ // building data is), read as changed themselves: a part kept from what they made is built again from the new file
2372
+ // rather than kept from the old one.
2373
+ // `named` gets each changed file outside the modules (data, or a script nothing imports, as a worker's) that the game's
2374
+ // code names, for what of a change nothing read again (`waitsFor`).
2375
+ async function namingData(paths, order, named = new Set()) {
2376
+ const outside = paths.filter(p => !/\.(html?|css)$/i.test(p) && !imports.has(p));
2377
+ const data = paths.filter(p => !CODE.test(p) && !loaded.has(p));
2378
+ if (!outside.length) return [];
2379
+ const naming = [];
2380
+ for (const path of order) {
2381
+ if (lampOwn(path) || paths.includes(path)) continue;
2382
+ const text = texts.get(path) ?? inline.get(path) ?? await readText(path).catch(() => '');
2383
+ for (const p of outside) if (names(text, p)) named.add(p);
2384
+ if (data.some(p => names(text, p))) naming.push(path);
2385
+ }
2386
+ return naming;
2387
+ }
2388
+
2389
+ // What of a change the code run again didn't take in: a file the game uses (one it loaded through the runtime, read in
2390
+ // any other way, or that its code names) that nothing read again, nor put in as it was (`put`), so a part kept from it,
2391
+ // or code that read it once, shows the old one until the page opens again. That waits for Reload.
2392
+ function waitsFor(change, named) {
2393
+ return (change.changed || []).map(clean).filter(p => !/\.(html?|css)$/i.test(p) && !imports.has(p) && !readSince.has(p) && !buildingFrom(p)
2394
+ && !(change.put || []).includes(p) && (loaded.has(p) || everRead.has(p) || named.has(p)));
2395
+ }
2396
+
2397
+ async function runAgain(change, state, roster) {
2398
+ const began = performance.now(), why = change.why;
2399
+ // What was built in the background waits for the next change that can run the code again.
2400
+ const cannot = reason => change.quiet ? log(`Lamp: ${why} waits for the next change (${reason}).`) : reopenOrHold({ ...change, why: `${why}; ${reason}` }, state);
2401
+ const paths = (change.changed || []).map(clean);
2402
+ let text, page;
2403
+ try { text = await readText(pagePath()); page = parse(text); } catch (e) { return cannot('its page couldn\'t be read'); }
2404
+ if (fixed(page) !== fixed(pageText != null ? parse(pageText) : document)) return cannot('its import map or a script that isn\'t a module changed');
2405
+ const classic = doc => [...doc.querySelectorAll('script[src]')].filter(s => s.type !== 'module' && s.type !== 'importmap')
2406
+ .map(s => pathOf(new URL(s.getAttribute('src'), location.href).href));
2407
+ const script = paths.find(p => classic(page).includes(p) || classic(document).includes(p));
2408
+ if (script) return cannot(`${script}, a script that isn't a module, changed`);
2409
+ // Every module of the game, in order, what each imports before it.
2410
+ const order = [];
2411
+ try {
2412
+ const roots = await readModules(paths.filter(p => /\.m?js$/i.test(p)), page);
2413
+ if (!roots.length) return cannot('the page has no module scripts');
2414
+ const seen = new Map();
2415
+ const visit = path => {
2416
+ if (seen.get(path) === 2) return;
2417
+ if (seen.get(path) === 1) throw new Error(`${path} is in a circle of modules that import each other`);
2418
+ seen.set(path, 1);
2419
+ for (const dep of imports.get(path) || []) visit(dep);
2420
+ seen.set(path, 2);
2421
+ order.push(path);
2422
+ };
2423
+ for (const root of roots) visit(root);
2424
+ } catch (e) { return cannot(e.message); }
2425
+ const named = new Set();
2426
+ markStale([...paths, ...await namingData(paths, order, named)]);
2427
+
2428
+ if (!change.quiet) {
2429
+ tellHost(change.gen, 'rerun', why);
2430
+ log(`Lamp: running the game's code again in place for a change (${why}), keeping where the player was.`);
2431
+ }
2432
+ // The frame on screen now, held there until the new code draws its first.
2433
+ const still = await new Promise(done => { shooting = done; after(250, () => { if (shooting === done) { shooting = null; done(null); } }); });
2434
+ // What Lamp holds is let go: the code it was pressing for is going. Its batch still has the controller.
2435
+ driven = null;
2436
+ const again = runningAgain = { change, state, began, called: false, done: null, named };
2437
+ readSince = new Set();
2438
+ const done = new Promise(resolve => { again.done = resolve; });
2439
+ // The old code stops: its frame loop, what it asked to have taken away, and what it left running.
2440
+ loops++;
2441
+ again.stopped = performance.now();
2442
+ started = false;
2443
+ // What's registered, as the last frame left it, or as it was before a swap that went wrong, which may have changed
2444
+ // it.
2445
+ restoring = !state ? null : change.touched ? state.held : heldNow();
2446
+ tally = { kept: [], built: [], reset: [], building: [], waiting: [], swapped: [] };
2447
+ if (state) again.state = { ...state, held: restoring };
2448
+ for (const entry of [...cameras.values(), ...states.values()]) entry.used = entry.settled = false;
2449
+ swapGuard = null;
2450
+ hidePill();
2451
+ holdPhones(roster);
2452
+ const leaving = hooks;
2453
+ hooks = {};
2454
+ try { if (leaving.dispose) await leaving.dispose(); } catch (e) { log(`Error: dispose: ${e && e.message || e}`); }
2455
+ for (const [path, m] of modules) {
2456
+ for (const dispose of m.disposers) { try { await dispose(m.data); } catch (e) { log(`Error: dispose in ${path}: ${e && e.message || e}`); } }
2457
+ m.disposers = [];
2458
+ }
2459
+ // A part whose async build started a timer still running loses it now, so it's built again.
2460
+ for (const k of kept.values()) if ((k.timers || []).some(t => intervals.has(t) || timeouts.has(t))) k.stale = 'a timer it started';
2461
+ takeAwayLeft();
2462
+ if (phonesWaiting) phonesWaiting.leaving = false;
2463
+ listeners.clear();
2464
+ for (const voice of voices) sound.stop(voice);
2465
+ voices.clear();
2466
+ if (musicVoice) { sound.stop(musicVoice); musicVoice = null; }
2467
+ for (const k of kept.values()) k.used = false;
2468
+ await putPage(page, paths, still);
2469
+ pageText = text;
2470
+ // The new code, each module once, in order, importing the others' new versions.
2471
+ const addresses = new Map();
2472
+ for (const path of order) {
2473
+ const own = inline.has(path), address = own ? location.href : `${addressOf(path)}?lamp=${++versionCount}`;
2474
+ loading.add(path);
2475
+ try {
2476
+ const code = own ? inline.get(path) : (texts.get(path) ?? await readText(path));
2477
+ texts.set(path, code);
2478
+ const base = own ? location.href : addressOf(path);
2479
+ const written = code.replace(SPECIFIER, (all, lead, quote, specifier) => {
2480
+ const target = resolve(specifier, base);
2481
+ if (!target) return all;
2482
+ const p = pathOf(target);
2483
+ return lead + quote + (p != null && addresses.has(p) ? addresses.get(p) : target) + quote;
2484
+ }).replace(/\bimport\.meta\b/g, `({ url: ${JSON.stringify(address)}, resolve: s => new URL(s, ${JSON.stringify(address)}).href })`);
2485
+ const from = URL.createObjectURL(new Blob([written], { type: 'text/javascript' }));
2486
+ addresses.set(path, from);
2487
+ if (rootPaths.includes(path)) {
2488
+ // The module that starts the game: what it awaits may be the whole start, so it isn't waited for here.
2489
+ if (again.started == null) again.started = performance.now();
2490
+ import(from).catch(e => ranAgainFailed(again, e));
2491
+ continue;
2492
+ }
2493
+ const ns = await import(from);
2494
+ const m = modules.get(path);
2495
+ if (m) { addVersion(m, versionOf(ns)); m.text = code; }
2496
+ latest.set(path, from);
2497
+ } catch (e) {
2498
+ return ranAgainFailed(again, `${path} didn't load (${e && e.message || e})`);
2499
+ } finally { loading.delete(path); }
2500
+ }
2501
+ for (const path of paths) texts.delete(path);
2502
+ for (const path of rootPaths) latest.set(path, addresses.get(path));
2503
+ after(120000, () => { if (runningAgain === again) ranAgainFailed(again, 'the game didn\'t start again within two minutes'); });
2504
+ return done;
2505
+ }
2506
+
2507
+ // The page as the new HTML has it: what isn't a script in its head (a stylesheet it already links stays, unless it
2508
+ // changed), and its body, with Lamp's own elements (the touch controls, the Reload button) kept.
2509
+ async function putPage(page, paths, still) {
2510
+ const head = document.head, links = [...head.querySelectorAll('link[rel~="stylesheet"]')], loads = [], placed = [];
2511
+ for (const node of [...page.head.children]) {
2512
+ if (node.tagName === 'SCRIPT') continue;
2513
+ if (node.matches('link[rel~="stylesheet"]')) {
2514
+ const path = pathOf(new URL(node.getAttribute('href'), location.href).href);
2515
+ const have = links.find(l => pathOf(l.href) === path && !placed.includes(l));
2516
+ if (have && !paths.includes(path)) { placed.push(have); continue; }
2517
+ const fresh = document.adoptNode(node);
2518
+ if (path != null) fresh.href = `${addressOf(path)}?lamp=${++versionCount}`;
2519
+ loads.push(new Promise(done => { fresh.onload = fresh.onerror = done; after(5000, done); }));
2520
+ placed.push(fresh);
2521
+ continue;
2522
+ }
2523
+ placed.push(document.adoptNode(node));
2524
+ }
2525
+ // In the new page's order, since the order of stylesheets is the order of their rules. A stylesheet already linked
2526
+ // moves only when it's out of that order (moved, it's read again); the scripts and what goes stay where they are
2527
+ // until the new stylesheets have loaded, so the page is never shown unstyled.
2528
+ let before = null;
2529
+ for (const node of placed) {
2530
+ let at = before ? before.nextSibling : head.firstChild;
2531
+ while (at && (at.nodeType !== 1 || at.tagName === 'SCRIPT' || !placed.includes(at))) at = at.nextSibling;
2532
+ if (node !== at) head.insertBefore(node, at);
2533
+ before = node;
2534
+ }
2535
+ await Promise.all(loads);
2536
+ for (const node of [...head.children]) if (node.tagName !== 'SCRIPT' && !placed.includes(node)) node.remove();
2537
+ const body = document.body;
2538
+ for (const a of [...body.attributes]) body.removeAttribute(a.name);
2539
+ for (const a of [...page.body.attributes]) body.setAttribute(a.name, a.value);
2540
+ // The old page stays in view, still, over the new one until the new code has drawn (`lift`): its elements go last,
2541
+ // so the new code finds its own by id, and the frame it showed stands where the canvas was.
2542
+ const canvas = renderer && renderer.domElement;
2543
+ const old = [...body.childNodes].filter(n => n !== touchLayer && n !== pill && n !== hint && n !== canvas && n !== curtain);
2544
+ if (curtain) curtain.remove();
2545
+ curtain = document.createElement('div');
2546
+ curtain.style.cssText = 'position:fixed;inset:0;z-index:2147483646;pointer-events:none';
2547
+ if (still) curtain.append(still);
2548
+ curtain.append(...old);
2549
+ // Scripts from the new page don't run as they go in: the modules run from runAgain.
2550
+ fromPage = [...page.body.childNodes].map(n => document.adoptNode(n));
2551
+ body.replaceChildren(...fromPage, curtain, ...[touchLayer, pill, hint].filter(Boolean));
2552
+ }
2553
+
2554
+ // The frame just drawn, as a picture placed where the game's canvas is, for runAgain to hold on screen; asked of the
2555
+ // next frame, since a WebGL canvas keeps what it drew only until the frame is shown.
2556
+ let shooting = null, curtain = null, lifting = 0;
2557
+ // What the page's HTML put back in at the last run in place, where a loading screen of the game's own is.
2558
+ let fromPage = [];
2559
+ // The change whose new code is about to be on screen, for `shown`: after a run in place as the old frame goes, after a
2560
+ // reload at the first frame.
2561
+ let shownFor = null;
2562
+ function shoot() {
2563
+ if (!shooting) return;
2564
+ const done = shooting;
2565
+ shooting = null;
2566
+ let still = null;
2567
+ try {
2568
+ const drawn = renderer && renderer.domElement;
2569
+ const canvas = drawn && (drawn.tagName === 'CANVAS' ? drawn : drawn.querySelector && drawn.querySelector('canvas'));
2570
+ if (canvas && canvas.width && canvas.height) {
2571
+ const r = canvas.getBoundingClientRect();
2572
+ still = document.createElement('canvas');
2573
+ still.width = canvas.width; still.height = canvas.height;
2574
+ still.getContext('2d').drawImage(canvas, 0, 0);
2575
+ still.style.cssText = `position:fixed;left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px`;
2576
+ }
2577
+ } catch (e) { still = null; }
2578
+ done(still);
2579
+ }
2580
+ // Once the new code has drawn a couple of frames, the old page held over it fades away.
2581
+ function lift() {
2582
+ if (!curtain || runningAgain || ++lifting < 3) return;
2583
+ // The change is on screen now: Lamp's light crosses the window as the old frame fades.
2584
+ if (shownFor != null) { tellHost(shownFor, 'shown'); shownFor = null; }
2585
+ const going = curtain;
2586
+ curtain = null;
2587
+ lifting = 0;
2588
+ // The page was put back as its HTML has it, so a game with a loading screen of its own there shows it again and
2589
+ // fades it out as it starts. That fade is finished under the old page, at once and for as long as the old page
2590
+ // takes to go, so the old page gives way to the game and never to its loading screen on the way out.
2591
+ putAway(going);
2592
+ for (const ms of [60, 120, 180, 240]) after(ms, () => putAway(going));
2593
+ going.style.transition = 'opacity 0.2s';
2594
+ going.style.opacity = '0';
2595
+ after(250, () => going.remove());
2596
+ }
2597
+
2598
+ // Finishes a screen of the page's own HTML being put away across most of the window (its loading screen, put back as
2599
+ // the page went in), which is a matter of a second at most. Only a fade out of what the HTML put back is: a scene the
2600
+ // game fades in, or one it times its play by and made itself, runs its course, as does anything small.
2601
+ function putAway(held) {
2602
+ try {
2603
+ if (typeof CSSTransition === 'undefined' || !document.getAnimations) return;
2604
+ const most = innerWidth * innerHeight / 2;
2605
+ for (const fade of document.getAnimations()) {
2606
+ const el = fade instanceof CSSTransition && fade.effect && fade.effect.target;
2607
+ if (!el || held.contains(el) || !['opacity', 'visibility'].includes(fade.transitionProperty)) continue;
2608
+ if (!fromPage.some(n => n === el || (n.contains && n.contains(el)))) continue;
2609
+ const frames = fade.effect.getKeyframes(), to = frames.length ? frames[frames.length - 1] : {};
2610
+ if (!(fade.transitionProperty === 'opacity' ? Number(to.opacity) === 0 : to.visibility === 'hidden')) continue;
2611
+ const timing = fade.effect.getComputedTiming(), r = el.getBoundingClientRect();
2612
+ if (!(timing.endTime <= 1500) || r.width * r.height < most) continue;
2613
+ fade.finish();
2614
+ }
2615
+ } catch (e) {}
2616
+ }
2617
+
2618
+ function ranAgain(again) {
2619
+ bringPhones();
2620
+ runningAgain = null;
2621
+ lifting = 0;
2622
+ // Putting in what was built in the background is the change's own going in, said by its report, not a change.
2623
+ shownFor = again.change.quiet ? null : again.change.gen;
2624
+ // No old frame held (a page with no canvas to picture): on screen at the next frame.
2625
+ if (!curtain && !again.change.quiet) after(0, () => { if (shownFor === again.change.gen) { tellHost(shownFor, 'shown'); shownFor = null; } });
2626
+ for (const [id, k] of kept) if (!k.used) { if (k.pending) stopBuilding(k); letGo(k); kept.delete(id); }
2627
+ // What a part built in the background replaced goes once the new one is on screen, not in its first frames.
2628
+ for (const k of kept.values()) if (k.used && k.replaces) { const old = k.replaces, spare = usedBy(k.made); k.replaces = null; after(1000, () => letGo(old, spare)); }
2629
+ for (const list of [cameras, states]) for (const [id, entry] of list) if (!entry.used) list.delete(id);
2630
+ restoring = null;
2631
+ const waits = waitsFor(again.change, again.named);
2632
+ const said = cameThrough(waits);
2633
+ // What was built again, each with how long it took and why, what's being built in the background, and what waits
2634
+ // for Reload: Lamp's chat and the agent's next look say it, so neither says the person sees what they don't. Said
2635
+ // once the new code's first frame is drawn, with how long the game stood still for it (`firstDrawn`).
2636
+ const ms = k => k.ms == null ? {} : { ms: Math.round(k.ms) }, cause = k => k.cause ? { cause: k.cause } : {};
2637
+ // The game's own start, outside the parts built again: what it awaits there (data fetched from the network, above
2638
+ // all) holds the game still at every change, and nothing else would tell the agent so.
2639
+ // Parts built side by side, or one inside another's build, overlap: what's taken off is the time some part was being
2640
+ // built, each moment once, within the start.
2641
+ const ranAt = performance.now();
2642
+ let outside = null;
2643
+ if (again.started != null) {
2644
+ const clipped = tally.built.filter(k => k.span).map(k => [Math.max(k.span[0], again.started), Math.min(k.span[1], ranAt)])
2645
+ .filter(([a, b]) => b > a).sort((x, y) => x[0] - y[0]);
2646
+ let building = 0, reach = again.started;
2647
+ for (const [a, b] of clipped) { if (b > reach) { building += b - Math.max(a, reach); reach = b; } }
2648
+ outside = Math.max(0, Math.round(ranAt - again.started - building));
2649
+ }
2650
+ reporting = { again, said, outside, ran: ranAt - again.began, more: {
2651
+ built: [...tally.built.map(k => ({ part: k.key, ...ms(k), ...cause(k) })),
2652
+ ...tally.building.map(k => ({ part: k.key, background: true, ...cause(k) })),
2653
+ ...tally.swapped.map(k => ({ part: k.key, background: true, ...ms(k), ...cause(k) }))],
2654
+ kept: tally.kept.length, waits, ...(outside == null ? {} : { outside }), ...(again.change.quiet ? { later: true } : {}) } };
2655
+ if (waits.length) {
2656
+ // The rest of the change goes in when the page opens again, at the person's Reload.
2657
+ waiting = { ...again.change, changed: waits, why: `nothing in the running game read ${waits.join(', ')} again`, hard: true, reopens: true };
2658
+ log(`Lamp: ${waits.join(', ')} changed, and nothing in the running game read ${waits.length === 1 ? 'it' : 'them'} again: that waits for Reload.`);
2659
+ if (again.change.pill) showPill();
2660
+ }
2661
+ swapGuard = { until: time + 1, change: again.change, state: again.state };
2662
+ again.done();
2663
+ }
2664
+
2665
+ // The new code's first frame is drawn: what came through is said, with how long the game stood still for it, from the
2666
+ // old code's last frame to this one, and that frame's own time.
2667
+ let reporting = null;
2668
+ function firstDrawn(cost) {
2669
+ if (!reporting) return;
2670
+ const { again, said, ran, outside, more } = reporting, still = performance.now() - again.stopped, compiled = warmed;
2671
+ reporting = warmed = null;
2672
+ const timing = `the game stood still ${Math.round(still)} ms (its first frame ${Math.round(cost)} ms`
2673
+ + `${compiled == null ? '' : `, drawn once its shaders had compiled in ${Math.round(compiled)} ms`})`;
2674
+ log(again.change.quiet ? `Lamp: put in what was built in the background${said ? ` (${said})` : ''}; ${timing}.`
2675
+ : `Lamp: the game's code ran again in place${said ? ` (${said})` : ''} in ${Math.round(ran)} ms`
2676
+ + `${outside >= 100 ? `, ${outside} ms of it in the game's own start outside its kept parts (what it awaits there, such as data from the network, holds it still at every change)` : ''}; ${timing}.`);
2677
+ tellHost(again.change.gen, 'report', said, { ...more, frozen: Math.round(still), firstFrame: Math.round(cost) });
2678
+ }
2679
+
2680
+ // New code that fails before it's running opens the page again, from the same place.
2681
+ function ranAgainFailed(again, e) {
2682
+ if (runningAgain !== again) return new Promise(() => {});
2683
+ runningAgain = null;
2684
+ log(`Error: ${e && e.message || e}`);
2685
+ reopen({ ...again.change, why: `the game's code run again failed (${e && e.message || e})` }, again.state);
2686
+ return new Promise(() => {});
2687
+ }
2688
+ // Until the new code is running (`ranAgain`), anything it throws opens the page again: half a start is no place to play.
2689
+ listen(window, 'error', e => { if (runningAgain) ranAgainFailed(runningAgain, e.error || e.message); });
2690
+ listen(window, 'unhandledrejection', e => { if (runningAgain) ranAgainFailed(runningAgain, e.reason); });
2691
+
2692
+ // What the game's code leaves running: each listener by its target (held weakly, so an element thrown away goes with
2693
+ // its listeners), timers, sound contexts and workers.
2694
+ const listening = new Set(), listenersOf = new WeakMap(), timeouts = new Set(), intervals = new Set(), owned = new Set();
2695
+ const captureOf = options => typeof options === 'boolean' ? options : !!(options && options.capture);
2696
+ function watch() {
2697
+ watching = true;
2698
+ // A call with no receiver (`addEventListener(...)` written without `window.`) is the window's, as it is to the
2699
+ // browser's own function, which also turns down anything that isn't an event target before it's recorded.
2700
+ EventTarget.prototype.addEventListener = function (type, fn, options) {
2701
+ const target = this ?? window;
2702
+ browser.listen.call(target, type, fn, options);
2703
+ if (fn == null || keeping) return;
2704
+ let list = listenersOf.get(target);
2705
+ if (!list) {
2706
+ listenersOf.set(target, list = []);
2707
+ listening.add(new WeakRef(target));
2708
+ if (listening.size > 4000) for (const ref of listening) if (!ref.deref()) listening.delete(ref);
2709
+ }
2710
+ const listener = { type, fn, capture: captureOf(options) };
2711
+ list.push(listener);
2712
+ for (const waiting of keepsWaiting) waiting.added.push([target, listener]);
2713
+ };
2714
+ EventTarget.prototype.removeEventListener = function (type, fn, options) {
2715
+ const target = this ?? window;
2716
+ browser.unlisten.call(target, type, fn, options);
2717
+ const list = listenersOf.get(target), capture = captureOf(options);
2718
+ if (list) { const i = list.findIndex(l => l.type === type && l.fn === fn && l.capture === capture); if (i >= 0) list.splice(i, 1); }
2719
+ };
2720
+ // A timer started while an async part's build waits is recorded against that part, and so is every timer its
2721
+ // callbacks start after (one that schedules its next tick), so the part is built again while any is running.
2722
+ const owners = () => [...[...keepsWaiting].map(waiting => waiting.timers), ...(timedFor || [])];
2723
+ const runFor = (lists, fn) => lists.length ? (...a) => { const was = timedFor; timedFor = lists; try { return fn(...a); } finally { timedFor = was; } } : fn;
2724
+ window.setTimeout = function (fn, ms, ...rest) {
2725
+ if (keeping || typeof fn !== 'function') return browser.setTimeout.call(window, fn, ms, ...rest);
2726
+ const lists = owners(), run = runFor(lists, fn);
2727
+ const id = browser.setTimeout.call(window, (...a) => { timeouts.delete(id); run(...a); }, ms, ...rest);
2728
+ timeouts.add(id);
2729
+ for (const list of lists) list.push(id);
2730
+ return id;
2731
+ };
2732
+ window.clearTimeout = id => { timeouts.delete(id); browser.clearTimeout.call(window, id); };
2733
+ window.setInterval = function (fn, ms, ...rest) {
2734
+ if (keeping || typeof fn !== 'function') return browser.setInterval.call(window, fn, ms, ...rest);
2735
+ const lists = owners();
2736
+ const id = browser.setInterval.call(window, runFor(lists, fn), ms, ...rest);
2737
+ intervals.add(id);
2738
+ for (const list of lists) list.push(id);
2739
+ return id;
2740
+ };
2741
+ window.clearInterval = id => { intervals.delete(id); browser.clearInterval.call(window, id); };
2742
+ const owning = (Made, end) => {
2743
+ const Watched = function (...args) {
2744
+ const made = new Made(...args);
2745
+ if (!keeping) {
2746
+ const ending = () => end(made);
2747
+ owned.add(ending);
2748
+ for (const waiting of keepsWaiting) waiting.made.push([made, ending]);
2749
+ }
2750
+ return made;
2751
+ };
2752
+ Watched.prototype = Made.prototype;
2753
+ return Watched;
2754
+ };
2755
+ if (browser.AudioContext) window.AudioContext = window.webkitAudioContext = owning(browser.AudioContext, c => c.state !== 'closed' && c.close());
2756
+ if (browser.Worker) {
2757
+ const Started = owning(browser.Worker, w => w.terminate());
2758
+ window.Worker = function (url, ...rest) {
2759
+ const path = pathOf(String(url));
2760
+ if (path != null) read(clean(path));
2761
+ workerStarted = true;
2762
+ return new Started(url, ...rest);
2763
+ };
2764
+ window.Worker.prototype = browser.Worker.prototype;
2765
+ }
2766
+ watchPhones();
2767
+ }
2768
+
2769
+ function takeAwayLeft() {
2770
+ for (const ref of listening) {
2771
+ const target = ref.deref();
2772
+ if (!target) continue;
2773
+ for (const { type, fn, capture } of listenersOf.get(target) || []) browser.unlisten.call(target, type, fn, capture);
2774
+ listenersOf.delete(target);
2775
+ }
2776
+ listening.clear();
2777
+ for (const id of timeouts) browser.clearTimeout.call(window, id);
2778
+ for (const id of intervals) browser.clearInterval.call(window, id);
2779
+ timeouts.clear(); intervals.clear();
2780
+ for (const end of owned) { try { end(); } catch (e) {} }
2781
+ owned.clear();
2782
+ }
2783
+
2784
+ // Phones belong to the page, as its link to them and their numbers do: running the game's code again neither stops nor
2785
+ // starts them, and lamp.js's timers go through the browser's own functions, so `takeAwayLeft` doesn't reach them. What
2786
+ // the code asked to hear of them belongs to that run of the code. The old run's calls end as it stops, and the new run's
2787
+ // wait until it's ready for its first frame: then it's told who left and who is here (`bringPhones`), and input goes on
2788
+ // from there. Presses and messages from in between are never replayed; what's held now is in `isHeld()`. A part the
2789
+ // game kept keeps hearing what it asked to, all along.
2790
+ const PHONE_EVENTS = ['onLink', 'onJoin', 'onLeave', 'onButton', 'onAxis', 'onMessage', 'onFile', 'onError'];
2791
+ const PHONE_LASTING = ['onLink', 'onError']; // about the session, not a player's input: nothing to wait for
2792
+ const phoneCalls = new Set();
2793
+ let phones = null, phonesWaiting = null;
2794
+
2795
+ function watchPhones() {
2796
+ phones = window.Lamp && window.Lamp.controllers;
2797
+ if (!phones) return;
2798
+ // A file that starts while no game's code can take it is turned down, so the phone says so rather than waiting.
2799
+ phones.onFile((player, file) => { if (phonesWaiting) file.cancel(); });
2800
+ for (const name of PHONE_EVENTS) {
2801
+ const add = phones[name];
2802
+ phones[name] = fn => {
2803
+ if (typeof fn !== 'function') return add.call(phones, fn);
2804
+ const call = { name, fn, kept: keeping > 0 };
2805
+ const off = add.call(phones, (...args) => {
2806
+ if (!phonesWaiting || call.kept || PHONE_LASTING.includes(name)) fn(...args);
2807
+ });
2808
+ call.end = () => { call.ended = true; phoneCalls.delete(call); off(); };
2809
+ if (!call.kept) phoneCalls.add(call);
2810
+ if (phonesWaiting) phonesWaiting.calls.push(call);
2811
+ return call.end;
2812
+ };
2813
+ }
2814
+ const stop = phones.stop;
2815
+ phones.stop = (...args) => {
2816
+ if (phonesWaiting && phonesWaiting.leaving) { log('Lamp: the phones stay joined while the game\'s code runs again.'); return; }
2817
+ return stop.apply(phones, args);
2818
+ };
2819
+ }
2820
+
2821
+ const phonesHere = () => phones ? phones.players() : [];
2822
+
2823
+ function holdPhones(players) {
2824
+ forgetPhoneEdges();
2825
+ if (!phones) return;
2826
+ phonesWaiting = { players, calls: [], leaving: true };
2827
+ for (const call of [...phoneCalls]) call.end();
2828
+ }
2829
+
2830
+ function bringPhones() {
2831
+ const waited = phonesWaiting;
2832
+ phonesWaiting = null;
2833
+ if (!waited) return;
2834
+ const here = phones.players();
2835
+ const tell = (name, player) => {
2836
+ for (const call of waited.calls) {
2837
+ if (call.name !== name || call.ended) continue;
2838
+ try { call.fn(player); } catch (e) { log(`Error: ${name}: ${e && e.message || e}`); }
2839
+ }
2840
+ };
2841
+ for (const player of waited.players) if (!here.includes(player)) tell('onLeave', player);
2842
+ for (const player of here) tell('onJoin', player);
2843
+ }
2844
+
2845
+ // Swapped code that fails in its first second: the page opens again from where the player was before the swap.
2846
+ function swapFailed(e) {
2847
+ const guard = swapGuard;
2848
+ swapGuard = null;
2849
+ reopen({ ...guard.change, why: `the code swapped in failed as it ran (${e && e.message || e})` }, guard.state);
2850
+ }
2851
+ window.addEventListener('error', e => { if (swapGuard) swapFailed(e.error || e.message); });
2852
+
2853
+ function keepLast() {
2854
+ if (!buildID) return;
2855
+ const snap = takeSnapshot();
2856
+ if (snap == null) return;
2857
+ try { store('local').setItem(LAST, JSON.stringify({ build: buildID, at: Date.now(), ...snap, input: current })); } catch (e) {}
2858
+ }
2859
+ window.addEventListener('pagehide', () => { if (started) keepLast(); });
2860
+ document.addEventListener('visibilitychange', () => { if (started && document.hidden) keepLast(); });
2861
+
2862
+ // Where this copy of the page starts: the place it kept for its reopening, or else the latest snapshot of an older
2863
+ // build (Lamp installed a new one since). Taken from storage as it's read, so it's restored at most once. A fresh start
2864
+ // Lamp asked for (`lampFreshStart`, set by the app around the page for the one launch it was asked for) restores
2865
+ // neither, and leaves the game's saves (save()) as they are: they're the person's.
2866
+ async function claim() {
2867
+ const session = store('session'), local = store('local');
2868
+ if (window.lampFreshStart) {
2869
+ try { session.removeItem(LIVE); local.removeItem(LAST); } catch (e) {}
2870
+ log('Lamp: a fresh start, as asked: the game starts from its beginning, with its saves kept.');
2871
+ return;
2872
+ }
2873
+ let said = null;
2874
+ try { said = JSON.parse(session.getItem(LIVE) || 'null'); session.removeItem(LIVE); reopened = !!said; } catch (e) {}
2875
+ if (said && said.fresh) { tellHost(said.gen, 'fresh'); return; }
2876
+ const id = await build;
2877
+ if (!said) {
2878
+ try {
2879
+ const last = JSON.parse(local.getItem(LAST) || 'null');
2880
+ // Within a quarter of an hour: an install that follows a change, not a game opened again another day.
2881
+ if (last && id && last.build !== id && Date.now() - last.at < 15 * 60 * 1000) { said = last; local.removeItem(LAST); }
2882
+ } catch (e) {}
2883
+ }
2884
+ if (said && said.input && said.input.kind) { current = said.input; padName = said.padName || ''; for (const changed of listeners) try { changed(input()); } catch (e) {} }
2885
+ pending = said;
2886
+ if (said && (said.state != null || said.held)) {
2887
+ // An error while the game starts from the state (in a Phaser scene's create, say, or the first frame) is the
2888
+ // state not taking.
2889
+ listen(window, 'error', e => { if (guarding) startFresh(e.error || e.message); });
2890
+ }
2891
+ }
2892
+
2893
+ // The state the game was in before a change of its code (what snapshot() gave), for the game to start from; null when
2894
+ // it starts fresh, and once it has started. Read it in init (or a Phaser scene's init or create) and build that place
2895
+ // instead of the title or intro: reading it is the restore, and restore() isn't called.
2896
+ export function pendingRestore() {
2897
+ asksPlace = true;
2898
+ if (started || !pending || pending.state == null) return null;
2899
+ taken = guarding = true;
2900
+ return pending.state;
2901
+ }
2902
+
2903
+ // After init: a game that didn't read the state is given it through restore(), before its first frame.
2904
+ async function putBack() {
2905
+ const said = pending;
2906
+ if (reopened && said && !said.fresh) shownFor = said.gen;
2907
+ if (taken) {
2908
+ log(reopened ? 'Lamp: the game started where it was, with the new code.' : 'Lamp: the game started where it was before it was installed again.');
2909
+ tellHost(said.gen, 'restored');
2910
+ } else if (said && said.state != null && hooks.restore) {
2911
+ guarding = true;
2912
+ try {
2913
+ await hooks.restore(said.state);
2914
+ log(reopened ? 'Lamp: the game is back where it was, with the new code.' : 'Lamp: the game is back where it was before it was installed again.');
2915
+ tellHost(said.gen, 'restored');
2916
+ } catch (e) { return startFresh(e); }
2917
+ } else if (said && said.held) {
2918
+ log(reopened ? 'Lamp: the game is back where it was, with the new code.' : 'Lamp: the game is back where it was before it was installed again.');
2919
+ tellHost(said.gen, 'restored');
2920
+ } else if (reopened && said && !said.fresh) tellHost(said.gen, 'started');
2921
+ if (reopened && said && !said.fresh && !runningAgain) {
2922
+ for (const entry of cameras.values()) if (said.held && said.held.cameras[entry.id]) tally.kept.push(`camera ${entry.id}`);
2923
+ tally.built.push(...[...kept.values()].map(k => ({ key: k.key, ms: k.ms, cause: null })));
2924
+ tellHost(said.gen, 'report', cameThrough(), {
2925
+ built: tally.built.map(k => ({ part: k.key, ...(k.ms == null ? {} : { ms: Math.round(k.ms) }) })), kept: tally.kept.length, waits: [] });
2926
+ }
2927
+ pending = null;
2928
+ restoring = null;
2929
+ started = true;
2930
+ }
2931
+
2932
+ // Half put back is worse than fresh: the page opens once more without the snapshot.
2933
+ function startFresh(e) {
2934
+ if (!guarding && !taken) return;
2935
+ guarding = taken = false;
2936
+ log(`Lamp: couldn't put the game back where it was (${e && e.message || e}), so it starts fresh.`);
2937
+ try { store('session').setItem(LIVE, JSON.stringify({ fresh: true, gen: pending && pending.gen })); } catch (e2) {}
2938
+ location.reload();
2939
+ return new Promise(() => {});
2940
+ }
2941
+
2942
+ function showPill() {
2943
+ if (pill) { pill.style.display = 'block'; return; }
2944
+ pill = document.createElement('button');
2945
+ pill.textContent = 'Reload to apply';
2946
+ pill.style.cssText = 'position:fixed;z-index:20;left:50%;transform:translateX(-50%);bottom:calc(14px + env(safe-area-inset-bottom));'
2947
+ + 'padding:8px 16px;border:0;border-radius:18px;background:rgba(20,20,24,.72);color:#fff;font:600 15px -apple-system,system-ui,sans-serif;'
2948
+ + '-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)';
2949
+ const press = e => { e.stopPropagation(); e.preventDefault(); if (waiting) { const change = { ...waiting, now: true }; waiting = null; queue = queue.then(() => change.reopens ? reopen(change) : startAgain(change)); } };
2950
+ listen(pill, 'pointerdown', press);
2951
+ document.body.append(pill);
2952
+ }
2953
+ function hidePill() { if (pill) pill.style.display = 'none'; }
2954
+
2955
+ // MARK: Lamp's hands on the controller
2956
+ //
2957
+ // Lamp's agent plays the game in its window, the one the person sees, with a controller of its own: `use_app`'s stick and
2958
+ // button moves, which the app around the page hands here (LampPageReader in LampPage.swift) as part of the same batch
2959
+ // as any of its clicks and keys (`LampDriver.Handover`). The app says when a batch with them begins (`__lampOwn`, with
2960
+ // how long it may run) and when it ends; for all that time, its waits and the gaps between presses too, Lamp has the
2961
+ // first player. While it holds something it is that player, in place of the player's own controller, keyboard and
2962
+ // touch, never mixed with them. Any input of the person's while Lamp has the controller (a controller, a key, the touch
2963
+ // screen, a phone's button, stick or tilt moved, or anything a phone sends) lets go of all of it and tells the app, which
2964
+ // ends the batch, so the game is theirs again at once. What Lamp holds also goes when its batch moves on, stops or is
2965
+ // replaced (the app hands null), when the game's code runs again or its page opens again, and by itself a second after
2966
+ // the hold was due to end; the batch's own hold on the controller ends with the time it was given and two seconds more,
2967
+ // should the app never say.
2968
+ let driven = null, drivenUntil = 0, lampHas = false, lampHasUntil = 0, lampBatch = null;
2969
+ // Each batch and each hold has a name of its own (the batch's id, and a number after it for each hold), and what gives
2970
+ // one back names it: a release that reaches the page late, after a newer batch or hold took the controller, is for one
2971
+ // that's gone already and lets go of nothing that's held now. A hold reaching the page after its batch ended, or after a
2972
+ // newer one took its place, is turned down ('ended'): nothing of a batch that's over is pressed.
2973
+ window.__lampOwn = (seconds, batch) => {
2974
+ if (!seconds) {
2975
+ if (batch != null && batch !== lampBatch) return true;
2976
+ lampHas = false; driven = null; lampBatch = null;
2977
+ return true;
2978
+ }
2979
+ if (!lampHas) takeController();
2980
+ lampHas = true;
2981
+ lampBatch = batch == null ? null : String(batch);
2982
+ lampHasUntil = clockNow() + Math.min(Math.max(Number(seconds) || 0, 0), 600) * 1000 + 2000;
2983
+ return true;
2984
+ };
2985
+ window.__lampDrive = (hold, name, batch) => {
2986
+ if (hold && batch != null && String(batch) !== lampBatch) return 'ended';
2987
+ if (!hold) {
2988
+ if (name != null && (!driven || driven.name !== String(name))) return true;
2989
+ driven = null;
2990
+ return true;
2991
+ }
2992
+ const axis = v => Math.max(-1, Math.min(1, Number(v) || 0)), trigger = v => Math.max(0, Math.min(1, Number(v) || 0));
2993
+ driven = { lx: axis(hold.lx), ly: axis(hold.ly), rx: axis(hold.rx), ry: axis(hold.ry), lt: trigger(hold.lt), rt: trigger(hold.rt), down: hold.down | 0 };
2994
+ // Kept off the pad itself, which the game reads.
2995
+ Object.defineProperty(driven, 'name', { value: name == null ? null : String(name) });
2996
+ drivenUntil = clockNow() + Math.min(Math.max(Number(hold.seconds) || 0, 0), 60) * 1000 + 1000;
2997
+ // A hold is Lamp's, whether or not the app said its batch began.
2998
+ if (!lampHas) takeController();
2999
+ if (!lampHas || lampHasUntil < drivenUntil) { lampHas = true; lampHasUntil = drivenUntil; }
3000
+ return true;
3001
+ };
3002
+ const pressing = pad => !!pad.down || ['lx', 'ly', 'rx', 'ry', 'lt', 'rt'].some(axis => Math.abs(pad[axis]) > 0.3);
3003
+ function letGoOfLamp(why) {
3004
+ if (!lampHas && !driven) return;
3005
+ lampHas = false;
3006
+ driven = null;
3007
+ lampBatch = null;
3008
+ if (!why) return;
3009
+ log(`Lamp: let go of the controller (${why}).`);
3010
+ try { const h = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.lampDriveTakenOver; h && h.postMessage(why); } catch (e) {}
3011
+ }
3012
+ // A phone's stick or tilt is a person's when it moves away from where it was as Lamp took the controller: one held still
3013
+ // at an angle (a phone lying tilted) says the same value over and over, and isn't someone playing, while a thumb pushing
3014
+ // the stick over slowly gets there however small each step is. A stick or tilt first heard while Lamp has the
3015
+ // controller is measured from the middle.
3016
+ const phoneAxes = new Map();
3017
+ let phoneAxesAtTake = new Map();
3018
+ function takeController() { phoneAxesAtTake = new Map(phoneAxes); }
3019
+ function phoneAxis(player, name, x, y) {
3020
+ const key = `${player} ${name}`, now = [Number(x) || 0, Number(y) || 0];
3021
+ phoneAxes.set(key, now);
3022
+ if (!lampHas) return;
3023
+ const from = phoneAxesAtTake.get(key) || [0, 0];
3024
+ if (Math.abs(now[0] - from[0]) > 0.3 || Math.abs(now[1] - from[1]) > 0.3) letGoOfLamp('a phone played');
3025
+ }
3026
+
3027
+ // Heard for as long as the page lasts, under what `watchPhones` wraps, so it belongs to no run of the game's code.
3028
+ if (window.Lamp && window.Lamp.controllers) {
3029
+ window.Lamp.controllers.onButton(phoneButton);
3030
+ window.Lamp.controllers.onLeave(player => { phoneButtons.delete(player); for (const axes of [phoneAxes, phoneAxesAtTake]) for (const key of axes.keys()) if (key.startsWith(`${player} `)) axes.delete(key); });
3031
+ if (window.Lamp.controllers.onAxis) window.Lamp.controllers.onAxis(phoneAxis);
3032
+ if (window.Lamp.controllers.onMessage) window.Lamp.controllers.onMessage(() => { if (lampHas) letGoOfLamp('a phone sent something'); });
3033
+ }
3034
+
3035
+ // Watched from here, after the runtime's own listeners: only where Lamp puts changes into the open game.
3036
+ if (host()) watch();