@thunderclap-engine/engine 0.0.0-stage → 1.1.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.
- package/CHANGELOG.md +390 -0
- package/LICENSE +21 -0
- package/README.md +333 -2
- package/dist/analytics/analytics.d.ts +80 -0
- package/dist/analytics/analytics.js +129 -0
- package/dist/analytics/index.d.ts +1 -0
- package/dist/analytics/index.js +1 -0
- package/dist/anim/aim.d.ts +43 -0
- package/dist/anim/aim.js +157 -0
- package/dist/anim/clip.d.ts +98 -0
- package/dist/anim/clip.js +282 -0
- package/dist/anim/compress.d.ts +65 -0
- package/dist/anim/compress.js +339 -0
- package/dist/anim/foot-ik.d.ts +74 -0
- package/dist/anim/foot-ik.js +241 -0
- package/dist/anim/graph.d.ts +153 -0
- package/dist/anim/graph.js +532 -0
- package/dist/anim/ik.d.ts +105 -0
- package/dist/anim/ik.js +435 -0
- package/dist/anim/index.d.ts +36 -0
- package/dist/anim/index.js +21 -0
- package/dist/anim/match.d.ts +66 -0
- package/dist/anim/match.js +153 -0
- package/dist/anim/mixer.d.ts +113 -0
- package/dist/anim/mixer.js +314 -0
- package/dist/anim/morph.d.ts +41 -0
- package/dist/anim/morph.js +94 -0
- package/dist/anim/pose.d.ts +74 -0
- package/dist/anim/pose.js +254 -0
- package/dist/anim/quat.d.ts +44 -0
- package/dist/anim/quat.js +187 -0
- package/dist/anim/retarget.d.ts +97 -0
- package/dist/anim/retarget.js +366 -0
- package/dist/anim/root-motion.d.ts +44 -0
- package/dist/anim/root-motion.js +224 -0
- package/dist/anim/skin-pose.d.ts +39 -0
- package/dist/anim/skin-pose.js +53 -0
- package/dist/anim/socket.d.ts +47 -0
- package/dist/anim/socket.js +84 -0
- package/dist/anim/spring-chain.d.ts +73 -0
- package/dist/anim/spring-chain.js +294 -0
- package/dist/assets/atlas.d.ts +90 -0
- package/dist/assets/atlas.js +212 -0
- package/dist/assets/baked.d.ts +33 -0
- package/dist/assets/baked.js +38 -0
- package/dist/assets/bundle.d.ts +88 -0
- package/dist/assets/bundle.js +464 -0
- package/dist/assets/cache.d.ts +56 -0
- package/dist/assets/cache.js +123 -0
- package/dist/assets/gltf-load.d.ts +252 -0
- package/dist/assets/gltf-load.js +661 -0
- package/dist/assets/gltf-skin.d.ts +36 -0
- package/dist/assets/gltf-skin.js +109 -0
- package/dist/assets/gltf.d.ts +706 -0
- package/dist/assets/gltf.js +1376 -0
- package/dist/assets/index.d.ts +20 -0
- package/dist/assets/index.js +10 -0
- package/dist/assets/ktx2.d.ts +71 -0
- package/dist/assets/ktx2.js +213 -0
- package/dist/assets/manifest.d.ts +79 -0
- package/dist/assets/manifest.js +7 -0
- package/dist/assets/meshopt.d.ts +23 -0
- package/dist/assets/meshopt.js +403 -0
- package/dist/audio/audio.d.ts +733 -0
- package/dist/audio/audio.js +2013 -0
- package/dist/audio/dsp.d.ts +61 -0
- package/dist/audio/dsp.js +159 -0
- package/dist/audio/duck.d.ts +44 -0
- package/dist/audio/duck.js +101 -0
- package/dist/audio/envelope.d.ts +53 -0
- package/dist/audio/envelope.js +106 -0
- package/dist/audio/index.d.ts +13 -0
- package/dist/audio/index.js +14 -0
- package/dist/audio/music.d.ts +113 -0
- package/dist/audio/music.js +548 -0
- package/dist/audio/occlusion.d.ts +55 -0
- package/dist/audio/occlusion.js +61 -0
- package/dist/audio/persist.d.ts +23 -0
- package/dist/audio/persist.js +27 -0
- package/dist/audio/recipes.d.ts +55 -0
- package/dist/audio/recipes.js +227 -0
- package/dist/audio/send.d.ts +41 -0
- package/dist/audio/send.js +118 -0
- package/dist/audio/sprites.d.ts +59 -0
- package/dist/audio/sprites.js +43 -0
- package/dist/audio/synth.d.ts +103 -0
- package/dist/audio/synth.js +197 -0
- package/dist/audio/variations.d.ts +25 -0
- package/dist/audio/variations.js +55 -0
- package/dist/audio/zones.d.ts +100 -0
- package/dist/audio/zones.js +167 -0
- package/dist/core/detached-surface.d.ts +44 -0
- package/dist/core/detached-surface.js +104 -0
- package/dist/core/events.d.ts +24 -0
- package/dist/core/events.js +43 -0
- package/dist/core/game.d.ts +409 -0
- package/dist/core/game.js +530 -0
- package/dist/core/hash.d.ts +24 -0
- package/dist/core/hash.js +49 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +9 -0
- package/dist/core/loop.d.ts +113 -0
- package/dist/core/loop.js +185 -0
- package/dist/core/math.d.ts +58 -0
- package/dist/core/math.js +210 -0
- package/dist/core/noise.d.ts +40 -0
- package/dist/core/noise.js +247 -0
- package/dist/core/pool.d.ts +42 -0
- package/dist/core/pool.js +85 -0
- package/dist/core/random.d.ts +61 -0
- package/dist/core/random.js +199 -0
- package/dist/core/scene.d.ts +79 -0
- package/dist/core/scene.js +195 -0
- package/dist/core/timer.d.ts +66 -0
- package/dist/core/timer.js +286 -0
- package/dist/fx/billboard-wgsl.d.ts +29 -0
- package/dist/fx/billboard-wgsl.js +158 -0
- package/dist/fx/billboard.d.ts +249 -0
- package/dist/fx/billboard.js +702 -0
- package/dist/fx/debug-draw.d.ts +127 -0
- package/dist/fx/debug-draw.js +523 -0
- package/dist/fx/debug-wgsl.d.ts +7 -0
- package/dist/fx/debug-wgsl.js +32 -0
- package/dist/fx/decals-wgsl.d.ts +15 -0
- package/dist/fx/decals-wgsl.js +173 -0
- package/dist/fx/decals.d.ts +216 -0
- package/dist/fx/decals.js +607 -0
- package/dist/fx/effect.d.ts +129 -0
- package/dist/fx/effect.js +349 -0
- package/dist/fx/effects/hit-spark.json +48 -0
- package/dist/fx/effects/index.d.ts +8 -0
- package/dist/fx/effects/index.js +9 -0
- package/dist/fx/effects/magic-aura.json +47 -0
- package/dist/fx/effects/smoke-puff.json +34 -0
- package/dist/fx/emit-surface.d.ts +40 -0
- package/dist/fx/emit-surface.js +124 -0
- package/dist/fx/flicker.d.ts +30 -0
- package/dist/fx/flicker.js +42 -0
- package/dist/fx/index.d.ts +31 -0
- package/dist/fx/index.js +20 -0
- package/dist/fx/particles-wgsl.d.ts +57 -0
- package/dist/fx/particles-wgsl.js +828 -0
- package/dist/fx/particles.d.ts +566 -0
- package/dist/fx/particles.js +1533 -0
- package/dist/fx/random.d.ts +15 -0
- package/dist/fx/random.js +20 -0
- package/dist/fx/sky-wgsl.d.ts +23 -0
- package/dist/fx/sky-wgsl.js +127 -0
- package/dist/fx/sky.d.ts +96 -0
- package/dist/fx/sky.js +175 -0
- package/dist/fx/text-atlas.d.ts +143 -0
- package/dist/fx/text-atlas.js +327 -0
- package/dist/fx/trail-wgsl.d.ts +11 -0
- package/dist/fx/trail-wgsl.js +42 -0
- package/dist/fx/trail.d.ts +194 -0
- package/dist/fx/trail.js +555 -0
- package/dist/fx/water-wgsl.d.ts +34 -0
- package/dist/fx/water-wgsl.js +197 -0
- package/dist/fx/water.d.ts +176 -0
- package/dist/fx/water.js +371 -0
- package/dist/geom/collide/bvh.d.ts +151 -0
- package/dist/geom/collide/bvh.js +493 -0
- package/dist/geom/collide/debug.d.ts +14 -0
- package/dist/geom/collide/debug.js +74 -0
- package/dist/geom/collide/grid.d.ts +26 -0
- package/dist/geom/collide/grid.js +91 -0
- package/dist/geom/collide/index.d.ts +8 -0
- package/dist/geom/collide/index.js +8 -0
- package/dist/geom/collide/queries.d.ts +70 -0
- package/dist/geom/collide/queries.js +583 -0
- package/dist/geom/collide/serialize.d.ts +14 -0
- package/dist/geom/collide/serialize.js +145 -0
- package/dist/geom/collide/shapes.d.ts +161 -0
- package/dist/geom/collide/shapes.js +686 -0
- package/dist/geom/collide/slide.d.ts +66 -0
- package/dist/geom/collide/slide.js +375 -0
- package/dist/geom/collide/tile-slide.d.ts +88 -0
- package/dist/geom/collide/tile-slide.js +255 -0
- package/dist/geom/geometry.d.ts +95 -0
- package/dist/geom/geometry.js +322 -0
- package/dist/geom/heightfield.d.ts +72 -0
- package/dist/geom/heightfield.js +295 -0
- package/dist/geom/hull.d.ts +27 -0
- package/dist/geom/hull.js +279 -0
- package/dist/geom/index.d.ts +35 -0
- package/dist/geom/index.js +23 -0
- package/dist/geom/iso.d.ts +74 -0
- package/dist/geom/iso.js +89 -0
- package/dist/geom/lod.d.ts +29 -0
- package/dist/geom/lod.js +27 -0
- package/dist/geom/ops.d.ts +47 -0
- package/dist/geom/ops.js +285 -0
- package/dist/geom/primitives.d.ts +97 -0
- package/dist/geom/primitives.js +359 -0
- package/dist/geom/shapes.d.ts +43 -0
- package/dist/geom/shapes.js +88 -0
- package/dist/geom/simplify.d.ts +38 -0
- package/dist/geom/simplify.js +519 -0
- package/dist/geom/split.d.ts +37 -0
- package/dist/geom/split.js +413 -0
- package/dist/geom/sweep.d.ts +83 -0
- package/dist/geom/sweep.js +534 -0
- package/dist/geom/tangents.d.ts +16 -0
- package/dist/geom/tangents.js +124 -0
- package/dist/geom/triangulate.d.ts +19 -0
- package/dist/geom/triangulate.js +147 -0
- package/dist/host/default.d.ts +10 -0
- package/dist/host/default.js +10 -0
- package/dist/host/index.d.ts +4 -0
- package/dist/host/index.js +3 -0
- package/dist/host/types.d.ts +144 -0
- package/dist/host/types.js +1 -0
- package/dist/host/web-links.d.ts +38 -0
- package/dist/host/web-links.js +110 -0
- package/dist/host/web.d.ts +30 -0
- package/dist/host/web.js +216 -0
- package/dist/host/xr.d.ts +24 -0
- package/dist/host/xr.js +59 -0
- package/dist/i18n/i18n.d.ts +59 -0
- package/dist/i18n/i18n.js +241 -0
- package/dist/i18n/index.d.ts +3 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/message.d.ts +72 -0
- package/dist/i18n/message.js +369 -0
- package/dist/i18n/negotiate.d.ts +17 -0
- package/dist/i18n/negotiate.js +58 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -0
- package/dist/input/bindings.d.ts +31 -0
- package/dist/input/bindings.js +141 -0
- package/dist/input/clock.d.ts +44 -0
- package/dist/input/clock.js +49 -0
- package/dist/input/follow.d.ts +133 -0
- package/dist/input/follow.js +218 -0
- package/dist/input/gestures.d.ts +281 -0
- package/dist/input/gestures.js +662 -0
- package/dist/input/glyphs.d.ts +35 -0
- package/dist/input/glyphs.js +216 -0
- package/dist/input/gyro.d.ts +85 -0
- package/dist/input/gyro.js +140 -0
- package/dist/input/index.d.ts +19 -0
- package/dist/input/index.js +16 -0
- package/dist/input/input.d.ts +543 -0
- package/dist/input/input.js +1492 -0
- package/dist/input/look.d.ts +327 -0
- package/dist/input/look.js +681 -0
- package/dist/input/magnify.d.ts +177 -0
- package/dist/input/magnify.js +131 -0
- package/dist/input/pointer-dispatch.d.ts +138 -0
- package/dist/input/pointer-dispatch.js +180 -0
- package/dist/input/pointer-router.d.ts +204 -0
- package/dist/input/pointer-router.js +533 -0
- package/dist/input/pointer-sources.d.ts +164 -0
- package/dist/input/pointer-sources.js +306 -0
- package/dist/input/rays.d.ts +138 -0
- package/dist/input/rays.js +615 -0
- package/dist/input/record.d.ts +209 -0
- package/dist/input/record.js +601 -0
- package/dist/input/touch-zones.d.ts +147 -0
- package/dist/input/touch-zones.js +300 -0
- package/dist/input/types.d.ts +158 -0
- package/dist/input/types.js +1 -0
- package/dist/input/xr-source.d.ts +53 -0
- package/dist/input/xr-source.js +164 -0
- package/dist/nav/astar.d.ts +45 -0
- package/dist/nav/astar.js +182 -0
- package/dist/nav/corridor.d.ts +40 -0
- package/dist/nav/corridor.js +95 -0
- package/dist/nav/crowd.d.ts +94 -0
- package/dist/nav/crowd.js +624 -0
- package/dist/nav/flow.d.ts +61 -0
- package/dist/nav/flow.js +193 -0
- package/dist/nav/grid.d.ts +108 -0
- package/dist/nav/grid.js +210 -0
- package/dist/nav/heap.d.ts +22 -0
- package/dist/nav/heap.js +92 -0
- package/dist/nav/index.d.ts +37 -0
- package/dist/nav/index.js +19 -0
- package/dist/nav/mesh/areas.d.ts +44 -0
- package/dist/nav/mesh/areas.js +98 -0
- package/dist/nav/mesh/bake-options.d.ts +33 -0
- package/dist/nav/mesh/bake-options.js +76 -0
- package/dist/nav/mesh/contour.d.ts +22 -0
- package/dist/nav/mesh/contour.js +323 -0
- package/dist/nav/mesh/flow3.d.ts +35 -0
- package/dist/nav/mesh/flow3.js +229 -0
- package/dist/nav/mesh/links.d.ts +38 -0
- package/dist/nav/mesh/links.js +72 -0
- package/dist/nav/mesh/mesh-path.d.ts +152 -0
- package/dist/nav/mesh/mesh-path.js +643 -0
- package/dist/nav/mesh/mesh-reach.d.ts +78 -0
- package/dist/nav/mesh/mesh-reach.js +450 -0
- package/dist/nav/mesh/navmesh.d.ts +117 -0
- package/dist/nav/mesh/navmesh.js +647 -0
- package/dist/nav/mesh/polymesh.d.ts +24 -0
- package/dist/nav/mesh/polymesh.js +384 -0
- package/dist/nav/mesh/regions.d.ts +12 -0
- package/dist/nav/mesh/regions.js +367 -0
- package/dist/nav/mesh/serialize.d.ts +13 -0
- package/dist/nav/mesh/serialize.js +197 -0
- package/dist/nav/mesh/voxelize.d.ts +111 -0
- package/dist/nav/mesh/voxelize.js +552 -0
- package/dist/nav/obstacles.d.ts +75 -0
- package/dist/nav/obstacles.js +294 -0
- package/dist/nav/path.d.ts +59 -0
- package/dist/nav/path.js +177 -0
- package/dist/nav/query-queue.d.ts +46 -0
- package/dist/nav/query-queue.js +308 -0
- package/dist/nav/reach.d.ts +53 -0
- package/dist/nav/reach.js +148 -0
- package/dist/nav/steer.d.ts +36 -0
- package/dist/nav/steer.js +78 -0
- package/dist/platform/boot.d.ts +28 -0
- package/dist/platform/boot.js +43 -0
- package/dist/platform/device-mode.d.ts +40 -0
- package/dist/platform/device-mode.js +104 -0
- package/dist/platform/device.d.ts +31 -0
- package/dist/platform/device.js +91 -0
- package/dist/platform/environment.d.ts +42 -0
- package/dist/platform/environment.js +82 -0
- package/dist/platform/gpu.d.ts +40 -0
- package/dist/platform/gpu.js +57 -0
- package/dist/platform/index.d.ts +18 -0
- package/dist/platform/index.js +10 -0
- package/dist/platform/letterbox.d.ts +43 -0
- package/dist/platform/letterbox.js +57 -0
- package/dist/platform/links.d.ts +20 -0
- package/dist/platform/links.js +43 -0
- package/dist/platform/screens.d.ts +36 -0
- package/dist/platform/screens.js +138 -0
- package/dist/platform/surface.d.ts +72 -0
- package/dist/platform/surface.js +157 -0
- package/dist/platform/xr-fake.d.ts +168 -0
- package/dist/platform/xr-fake.js +434 -0
- package/dist/platform/xr-surface.d.ts +120 -0
- package/dist/platform/xr-surface.js +214 -0
- package/dist/platform/xr-types.d.ts +262 -0
- package/dist/post/ao.d.ts +12 -0
- package/dist/post/ao.js +136 -0
- package/dist/post/blend.d.ts +27 -0
- package/dist/post/blend.js +129 -0
- package/dist/post/depth-pass.d.ts +43 -0
- package/dist/post/depth-pass.js +118 -0
- package/dist/post/dof.d.ts +1 -0
- package/dist/post/dof.js +66 -0
- package/dist/post/exposure.d.ts +80 -0
- package/dist/post/exposure.js +322 -0
- package/dist/post/index.d.ts +12 -0
- package/dist/post/index.js +11 -0
- package/dist/post/lens-dirt.d.ts +13 -0
- package/dist/post/lens-dirt.js +91 -0
- package/dist/post/lut.d.ts +19 -0
- package/dist/post/lut.js +50 -0
- package/dist/post/motion-blur.d.ts +9 -0
- package/dist/post/motion-blur.js +78 -0
- package/dist/post/post-stack.d.ts +302 -0
- package/dist/post/post-stack.js +1137 -0
- package/dist/post/settings.d.ts +485 -0
- package/dist/post/settings.js +416 -0
- package/dist/post/shaders.d.ts +25 -0
- package/dist/post/shaders.js +410 -0
- package/dist/post/ssr.d.ts +16 -0
- package/dist/post/ssr.js +155 -0
- package/dist/post/taa.d.ts +74 -0
- package/dist/post/taa.js +235 -0
- package/dist/post/white-balance.d.ts +25 -0
- package/dist/post/white-balance.js +98 -0
- package/dist/render/bindings.d.ts +98 -0
- package/dist/render/bindings.js +281 -0
- package/dist/render/blend.d.ts +13 -0
- package/dist/render/blend.js +26 -0
- package/dist/render/camera-2d.d.ts +72 -0
- package/dist/render/camera-2d.js +92 -0
- package/dist/render/camera.d.ts +302 -0
- package/dist/render/camera.js +671 -0
- package/dist/render/color.d.ts +16 -0
- package/dist/render/color.js +41 -0
- package/dist/render/compressed.d.ts +104 -0
- package/dist/render/compressed.js +248 -0
- package/dist/render/diagnostics.d.ts +15 -0
- package/dist/render/diagnostics.js +43 -0
- package/dist/render/dynamic-resolution.d.ts +70 -0
- package/dist/render/dynamic-resolution.js +126 -0
- package/dist/render/fake-gpu.d.ts +204 -0
- package/dist/render/fake-gpu.js +426 -0
- package/dist/render/flipbook.d.ts +56 -0
- package/dist/render/flipbook.js +87 -0
- package/dist/render/frame.d.ts +278 -0
- package/dist/render/frame.js +174 -0
- package/dist/render/fullscreen.d.ts +54 -0
- package/dist/render/fullscreen.js +91 -0
- package/dist/render/gpu-timer.d.ts +73 -0
- package/dist/render/gpu-timer.js +186 -0
- package/dist/render/index.d.ts +53 -0
- package/dist/render/index.js +29 -0
- package/dist/render/instance-list.d.ts +185 -0
- package/dist/render/instance-list.js +547 -0
- package/dist/render/lights.d.ts +181 -0
- package/dist/render/lights.js +233 -0
- package/dist/render/lod.d.ts +54 -0
- package/dist/render/lod.js +52 -0
- package/dist/render/material.d.ts +165 -0
- package/dist/render/material.js +292 -0
- package/dist/render/mesh.d.ts +284 -0
- package/dist/render/mesh.js +472 -0
- package/dist/render/mipmap.d.ts +16 -0
- package/dist/render/mipmap.js +121 -0
- package/dist/render/parallax.d.ts +50 -0
- package/dist/render/parallax.js +47 -0
- package/dist/render/pbr-wgsl.d.ts +31 -0
- package/dist/render/pbr-wgsl.js +119 -0
- package/dist/render/ping-pong.d.ts +42 -0
- package/dist/render/ping-pong.js +74 -0
- package/dist/render/renderer.d.ts +741 -0
- package/dist/render/renderer.js +2486 -0
- package/dist/render/scene-pipelines.d.ts +44 -0
- package/dist/render/scene-pipelines.js +92 -0
- package/dist/render/scene-target.d.ts +170 -0
- package/dist/render/scene-target.js +450 -0
- package/dist/render/shadow.d.ts +74 -0
- package/dist/render/shadow.js +208 -0
- package/dist/render/skin.d.ts +112 -0
- package/dist/render/skin.js +240 -0
- package/dist/render/snapshot.d.ts +36 -0
- package/dist/render/snapshot.js +34 -0
- package/dist/render/standard.d.ts +162 -0
- package/dist/render/standard.js +263 -0
- package/dist/render/texture.d.ts +142 -0
- package/dist/render/texture.js +228 -0
- package/dist/render/tile-layer.d.ts +110 -0
- package/dist/render/tile-layer.js +304 -0
- package/dist/render/transform.d.ts +115 -0
- package/dist/render/transform.js +318 -0
- package/dist/render/wgsl.d.ts +131 -0
- package/dist/render/wgsl.js +528 -0
- package/dist/save/codec.d.ts +49 -0
- package/dist/save/codec.js +169 -0
- package/dist/save/index.d.ts +8 -0
- package/dist/save/index.js +8 -0
- package/dist/save/journal.d.ts +24 -0
- package/dist/save/journal.js +102 -0
- package/dist/save/migrate.d.ts +15 -0
- package/dist/save/migrate.js +37 -0
- package/dist/save/schema.d.ts +72 -0
- package/dist/save/schema.js +154 -0
- package/dist/save/settings.d.ts +28 -0
- package/dist/save/settings.js +59 -0
- package/dist/save/slots.d.ts +56 -0
- package/dist/save/slots.js +200 -0
- package/dist/save/storage.d.ts +35 -0
- package/dist/save/storage.js +62 -0
- package/dist/save/store.d.ts +114 -0
- package/dist/save/store.js +269 -0
- package/dist/testing/fake-host.d.ts +103 -0
- package/dist/testing/fake-host.js +163 -0
- package/dist/testing/fixed-clock.d.ts +25 -0
- package/dist/testing/fixed-clock.js +30 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/test-game.d.ts +76 -0
- package/dist/testing/test-game.js +92 -0
- package/dist/tween/ease.d.ts +77 -0
- package/dist/tween/ease.js +192 -0
- package/dist/tween/index.d.ts +8 -0
- package/dist/tween/index.js +7 -0
- package/dist/tween/timeline.d.ts +78 -0
- package/dist/tween/timeline.js +269 -0
- package/dist/tween/tween.d.ts +168 -0
- package/dist/tween/tween.js +320 -0
- package/dist/ui/anchor.d.ts +206 -0
- package/dist/ui/anchor.js +307 -0
- package/dist/ui/caption.d.ts +122 -0
- package/dist/ui/caption.js +286 -0
- package/dist/ui/cursor.d.ts +105 -0
- package/dist/ui/cursor.js +261 -0
- package/dist/ui/dom.d.ts +30 -0
- package/dist/ui/dom.js +92 -0
- package/dist/ui/floats.d.ts +86 -0
- package/dist/ui/floats.js +260 -0
- package/dist/ui/hud.d.ts +198 -0
- package/dist/ui/hud.js +431 -0
- package/dist/ui/index.d.ts +37 -0
- package/dist/ui/index.js +34 -0
- package/dist/ui/inventory-card.d.ts +52 -0
- package/dist/ui/inventory-card.js +173 -0
- package/dist/ui/inventory-context.d.ts +29 -0
- package/dist/ui/inventory-context.js +125 -0
- package/dist/ui/inventory-drag.d.ts +63 -0
- package/dist/ui/inventory-drag.js +394 -0
- package/dist/ui/inventory-grid.d.ts +66 -0
- package/dist/ui/inventory-grid.js +625 -0
- package/dist/ui/inventory-radial.d.ts +39 -0
- package/dist/ui/inventory-radial.js +168 -0
- package/dist/ui/inventory.d.ts +128 -0
- package/dist/ui/inventory.js +154 -0
- package/dist/ui/layout.d.ts +356 -0
- package/dist/ui/layout.js +856 -0
- package/dist/ui/link.d.ts +64 -0
- package/dist/ui/link.js +155 -0
- package/dist/ui/loading.d.ts +40 -0
- package/dist/ui/loading.js +157 -0
- package/dist/ui/loupe.d.ts +74 -0
- package/dist/ui/loupe.js +164 -0
- package/dist/ui/menu-nav.d.ts +33 -0
- package/dist/ui/menu-nav.js +28 -0
- package/dist/ui/minimap-canvas.d.ts +145 -0
- package/dist/ui/minimap-canvas.js +460 -0
- package/dist/ui/notify.d.ts +40 -0
- package/dist/ui/notify.js +141 -0
- package/dist/ui/orb.d.ts +53 -0
- package/dist/ui/orb.js +304 -0
- package/dist/ui/overlay.d.ts +213 -0
- package/dist/ui/overlay.js +394 -0
- package/dist/ui/padnav.d.ts +53 -0
- package/dist/ui/padnav.js +146 -0
- package/dist/ui/perf.d.ts +130 -0
- package/dist/ui/perf.js +329 -0
- package/dist/ui/rotate-card.d.ts +54 -0
- package/dist/ui/rotate-card.js +97 -0
- package/dist/ui/stack.d.ts +128 -0
- package/dist/ui/stack.js +312 -0
- package/dist/ui/styles.d.ts +13 -0
- package/dist/ui/styles.js +200 -0
- package/dist/ui/text-scale.d.ts +67 -0
- package/dist/ui/text-scale.js +102 -0
- package/dist/ui/theme.d.ts +70 -0
- package/dist/ui/theme.js +98 -0
- package/dist/ui/toast.d.ts +23 -0
- package/dist/ui/toast.js +46 -0
- package/dist/ui/tooltip.d.ts +89 -0
- package/dist/ui/tooltip.js +333 -0
- package/dist/ui/widgets-2.d.ts +134 -0
- package/dist/ui/widgets-2.js +399 -0
- package/dist/ui/widgets-3.d.ts +91 -0
- package/dist/ui/widgets-3.js +212 -0
- package/dist/ui/widgets.d.ts +34 -0
- package/dist/ui/widgets.js +50 -0
- package/dist/ui/world-panel/index.d.ts +5 -0
- package/dist/ui/world-panel/index.js +5 -0
- package/dist/ui/world-panel/layout.d.ts +38 -0
- package/dist/ui/world-panel/layout.js +68 -0
- package/dist/ui/world-panel/paint.d.ts +96 -0
- package/dist/ui/world-panel/paint.js +136 -0
- package/dist/ui/world-panel/panel.d.ts +235 -0
- package/dist/ui/world-panel/panel.js +762 -0
- package/dist/ui/world-panel/wgsl.d.ts +10 -0
- package/dist/ui/world-panel/wgsl.js +52 -0
- package/dist/ui/world-panel/widgets.d.ts +207 -0
- package/dist/ui/world-panel/widgets.js +569 -0
- package/dist/voxel/chunks.d.ts +77 -0
- package/dist/voxel/chunks.js +149 -0
- package/dist/voxel/grid.d.ts +84 -0
- package/dist/voxel/grid.js +223 -0
- package/dist/voxel/index.d.ts +22 -0
- package/dist/voxel/index.js +7 -0
- package/dist/voxel/mesher-worker.d.ts +14 -0
- package/dist/voxel/mesher-worker.js +47 -0
- package/dist/voxel/mesher.d.ts +73 -0
- package/dist/voxel/mesher.js +306 -0
- package/dist/voxel/ray.d.ts +32 -0
- package/dist/voxel/ray.js +73 -0
- package/dist/voxel/wgsl.d.ts +16 -0
- package/dist/voxel/wgsl.js +28 -0
- package/dist/voxel/worker-client.d.ts +79 -0
- package/dist/voxel/worker-client.js +122 -0
- package/package.json +177 -3
|
@@ -0,0 +1,1492 @@
|
|
|
1
|
+
import { Emitter } from '../core/events.js';
|
|
2
|
+
import { bindingMatchesDevice, glyphForBinding, guessPadLayout, } from './glyphs.js';
|
|
3
|
+
import { createPointerDispatcher, POINTER_PRIORITY, } from './pointer-dispatch.js';
|
|
4
|
+
const EMPTY_BINDINGS = [];
|
|
5
|
+
/**
|
|
6
|
+
* Shape a stick (x, y) through a deadzone into `out` and return it (ws/input-5). Pure and
|
|
7
|
+
* allocation-free; `Input.stick` reads four actions through it and `Look` reads the pad sticks
|
|
8
|
+
* and the touch stick through it. See `StickOptions` for the shapes.
|
|
9
|
+
*/
|
|
10
|
+
export function shapeStick(x, y, out, opts) {
|
|
11
|
+
const dz = opts?.deadzone ?? 0.15;
|
|
12
|
+
const shape = opts?.shape ?? 'scaledRadial';
|
|
13
|
+
const curve = opts?.curve ?? 1;
|
|
14
|
+
const clamp = opts?.clamp ?? true;
|
|
15
|
+
if (shape === 'axial') {
|
|
16
|
+
let ax = Math.abs(x) <= dz ? 0 : x;
|
|
17
|
+
let ay = Math.abs(y) <= dz ? 0 : y;
|
|
18
|
+
if (curve !== 1) {
|
|
19
|
+
ax = Math.sign(ax) * Math.abs(ax) ** curve;
|
|
20
|
+
ay = Math.sign(ay) * Math.abs(ay) ** curve;
|
|
21
|
+
}
|
|
22
|
+
const m = Math.hypot(ax, ay);
|
|
23
|
+
if (clamp && m > 1) {
|
|
24
|
+
ax /= m;
|
|
25
|
+
ay /= m;
|
|
26
|
+
}
|
|
27
|
+
out[0] = ax;
|
|
28
|
+
out[1] = ay;
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
const m = Math.hypot(x, y);
|
|
32
|
+
if (m <= dz || m === 0) {
|
|
33
|
+
out[0] = 0;
|
|
34
|
+
out[1] = 0;
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
const mag = clamp ? Math.min(1, m) : m;
|
|
38
|
+
let len;
|
|
39
|
+
if (shape === 'radial')
|
|
40
|
+
len = mag;
|
|
41
|
+
else
|
|
42
|
+
len = dz >= 1 ? 0 : (mag - dz) / (1 - dz);
|
|
43
|
+
if (curve !== 1)
|
|
44
|
+
len **= curve;
|
|
45
|
+
const k = len / m;
|
|
46
|
+
out[0] = x * k;
|
|
47
|
+
out[1] = y * k;
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/** Default `holdMs` for `trigger: 'hold'`. */
|
|
51
|
+
export const DEFAULT_HOLD_MS = 400;
|
|
52
|
+
/** Default `doubleTapMs` for `trigger: 'doubleTap'`. */
|
|
53
|
+
export const DEFAULT_DOUBLE_TAP_MS = 300;
|
|
54
|
+
const DEFAULT_AXIS_THRESHOLD = 0.5;
|
|
55
|
+
const DEFAULT_BUTTON_THRESHOLD = 0.5;
|
|
56
|
+
/** Most repeat taps queued per key/button. */
|
|
57
|
+
const MAX_QUEUED_TAPS = 8;
|
|
58
|
+
const XR_BUTTONS = ['trigger', 'squeeze', 'pinch'];
|
|
59
|
+
function newTriggerState() {
|
|
60
|
+
return { wasDown: false, downTicks: 0, lastDownTick: -1, latched: false };
|
|
61
|
+
}
|
|
62
|
+
const NON_TEXT_INPUTS = new Set([
|
|
63
|
+
'button',
|
|
64
|
+
'submit',
|
|
65
|
+
'reset',
|
|
66
|
+
'checkbox',
|
|
67
|
+
'radio',
|
|
68
|
+
'image',
|
|
69
|
+
'range',
|
|
70
|
+
'color',
|
|
71
|
+
'file',
|
|
72
|
+
]);
|
|
73
|
+
/** True for elements where keys type text (text inputs, textareas, selects, contenteditable). */
|
|
74
|
+
export function isTextEntryTarget(target) {
|
|
75
|
+
const el = target;
|
|
76
|
+
if (!el || typeof el.tagName !== 'string')
|
|
77
|
+
return false;
|
|
78
|
+
if (el.isContentEditable)
|
|
79
|
+
return true;
|
|
80
|
+
switch (el.tagName) {
|
|
81
|
+
case 'TEXTAREA':
|
|
82
|
+
case 'SELECT':
|
|
83
|
+
return true;
|
|
84
|
+
case 'INPUT':
|
|
85
|
+
return !NON_TEXT_INPUTS.has((el.type ?? '').toLowerCase());
|
|
86
|
+
}
|
|
87
|
+
const role = el.getAttribute?.('role');
|
|
88
|
+
return role === 'textbox' || role === 'searchbox' || role === 'combobox';
|
|
89
|
+
}
|
|
90
|
+
const BUTTON_INPUTS = new Set(['button', 'submit', 'reset', 'image']);
|
|
91
|
+
/**
|
|
92
|
+
* True for plain activatable controls (buttons, links, `summary`, button-type inputs,
|
|
93
|
+
* `role=button|link|menuitem`) whose own keys are only Enter and Space.
|
|
94
|
+
*/
|
|
95
|
+
export function isButtonTarget(target) {
|
|
96
|
+
const el = target;
|
|
97
|
+
if (!el || typeof el.tagName !== 'string')
|
|
98
|
+
return false;
|
|
99
|
+
switch (el.tagName) {
|
|
100
|
+
case 'BUTTON':
|
|
101
|
+
case 'A':
|
|
102
|
+
case 'SUMMARY':
|
|
103
|
+
return true;
|
|
104
|
+
case 'INPUT':
|
|
105
|
+
return BUTTON_INPUTS.has((el.type ?? '').toLowerCase());
|
|
106
|
+
}
|
|
107
|
+
const role = el.getAttribute?.('role');
|
|
108
|
+
return role === 'button' || role === 'link' || role === 'menuitem';
|
|
109
|
+
}
|
|
110
|
+
function defaultGamepads() {
|
|
111
|
+
try {
|
|
112
|
+
return typeof navigator !== 'undefined' && navigator.getGamepads ? navigator.getGamepads() : [];
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Permissions policy can forbid the Gamepad API inside iframes.
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** True for elements that own their keyboard input (text fields, buttons, links, …). */
|
|
120
|
+
export function isInteractiveTarget(target) {
|
|
121
|
+
const el = target;
|
|
122
|
+
if (!el || typeof el.tagName !== 'string')
|
|
123
|
+
return false;
|
|
124
|
+
if (el.isContentEditable)
|
|
125
|
+
return true;
|
|
126
|
+
switch (el.tagName) {
|
|
127
|
+
case 'INPUT':
|
|
128
|
+
case 'TEXTAREA':
|
|
129
|
+
case 'SELECT':
|
|
130
|
+
case 'BUTTON':
|
|
131
|
+
case 'A':
|
|
132
|
+
case 'SUMMARY':
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
const role = el.getAttribute?.('role');
|
|
136
|
+
return role === 'button' || role === 'textbox' || role === 'slider' || role === 'menuitem';
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Action-mapped input. Feed it raw events (via `attach` or the low-level methods), call
|
|
140
|
+
* `update()` once per simulation tick, then query actions.
|
|
141
|
+
*
|
|
142
|
+
* ```ts
|
|
143
|
+
* const input = new Input({ confirm: [{ key: 'Enter' }, { key: 'Space' }, { padButton: 0 }],
|
|
144
|
+
* cancel: [{ key: 'Escape' }, { padButton: 1 }] });
|
|
145
|
+
* input.attach({ pointerTarget: canvas });
|
|
146
|
+
* input.setPointerTransform(platform.clientToLogical);
|
|
147
|
+
* // each tick:
|
|
148
|
+
* input.update();
|
|
149
|
+
* if (input.pressed('confirm')) …
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* `A` is the full action union. The base map may leave some out (ws/input-12): an action only a
|
|
153
|
+
* pushed context binds (`new Input<'jump' | 'back'>({ jump }, …)`, then
|
|
154
|
+
* `pushContext('menu', { bindings: { back } })`) is typed everywhere and reads inactive until a
|
|
155
|
+
* context binds it. `bindings()` lists the actions bound so far.
|
|
156
|
+
*/
|
|
157
|
+
export class Input {
|
|
158
|
+
pointer = {
|
|
159
|
+
x: 0,
|
|
160
|
+
y: 0,
|
|
161
|
+
dx: 0,
|
|
162
|
+
dy: 0,
|
|
163
|
+
inside: false,
|
|
164
|
+
down: false,
|
|
165
|
+
wheelX: 0,
|
|
166
|
+
wheelY: 0,
|
|
167
|
+
type: 'mouse',
|
|
168
|
+
};
|
|
169
|
+
blocked;
|
|
170
|
+
/** Pad button naming for `glyphFor` (`InputOptions.padLayout`); `null` guesses from the pad's `id`. */
|
|
171
|
+
padLayout;
|
|
172
|
+
/** Ms per tick (`InputOptions.tickMs`). */
|
|
173
|
+
tickMs;
|
|
174
|
+
/** The recorder capturing this input's feed (`createInputRecorder`), until `stop()`. */
|
|
175
|
+
recorder = null;
|
|
176
|
+
/** The playback feeding this input (`createInputPlayback`), until `detach()`. */
|
|
177
|
+
playback = null;
|
|
178
|
+
/** Feed hooks in install order (`onFeed`). Read-only for callers. */
|
|
179
|
+
feedHooks = [];
|
|
180
|
+
/** The base bindings (below every context). */
|
|
181
|
+
map;
|
|
182
|
+
/** The bindings in force: `map` with the context stack resolved over it (`map` when empty). */
|
|
183
|
+
active;
|
|
184
|
+
/** Pushed contexts, bottom first. */
|
|
185
|
+
stack = [];
|
|
186
|
+
/** Actions a context added that the base map lacks (`pushContext`); bound to nothing below it. */
|
|
187
|
+
extraActions = false;
|
|
188
|
+
/** `stack`'s names, bottom first (the `contexts` getter). */
|
|
189
|
+
contextNames = [];
|
|
190
|
+
/** The latest press per action (`buffered`, `consume`). */
|
|
191
|
+
presses = new Map();
|
|
192
|
+
/** True while `replayPointer` dispatches: the feed consumer then leaves the raw state alone. */
|
|
193
|
+
injecting = false;
|
|
194
|
+
states = new Map();
|
|
195
|
+
triggers = new Map();
|
|
196
|
+
events = new Emitter();
|
|
197
|
+
deviceValue = 'keyboard';
|
|
198
|
+
/** Ticks since construction (the trigger clock). */
|
|
199
|
+
tick = 0;
|
|
200
|
+
listenState = null;
|
|
201
|
+
/** Last client-space pointer position fed through `pointerAt` (the orbit drag's coordinates). */
|
|
202
|
+
clientX = 0;
|
|
203
|
+
clientY = 0;
|
|
204
|
+
claimFn = (_id, device) => this.linkedLook?.claimsPointer(device) ?? false;
|
|
205
|
+
getGamepads;
|
|
206
|
+
transform = null;
|
|
207
|
+
// Raw state.
|
|
208
|
+
keysDown = new Set();
|
|
209
|
+
keysTapped = new Set();
|
|
210
|
+
buttonsDown = new Set();
|
|
211
|
+
buttonsTapped = new Set();
|
|
212
|
+
/** Extra taps queued per key/button (see `InputOptions.queueTaps`). */
|
|
213
|
+
keyRepeats = new Map();
|
|
214
|
+
buttonRepeats = new Map();
|
|
215
|
+
queueTaps;
|
|
216
|
+
globalKeys = new Set();
|
|
217
|
+
buttonKeys = new Set();
|
|
218
|
+
wheelAccX = 0;
|
|
219
|
+
wheelAccY = 0;
|
|
220
|
+
prevX = 0;
|
|
221
|
+
prevY = 0;
|
|
222
|
+
boundKeys = new Set();
|
|
223
|
+
boundMouse = new Set();
|
|
224
|
+
boundWheel = false;
|
|
225
|
+
pads = [];
|
|
226
|
+
/** `this.pads`, bound once, for a linked look (`Look.padsFromInput`). */
|
|
227
|
+
readTickPads = () => this.pads;
|
|
228
|
+
/** Keys and mouse buttons that were down or tapped on the last tick. */
|
|
229
|
+
prevKeys = new Set();
|
|
230
|
+
prevButtons = new Set();
|
|
231
|
+
detachFn = null;
|
|
232
|
+
linkedGestures = null;
|
|
233
|
+
linkedPointers = null;
|
|
234
|
+
linkedLook = null;
|
|
235
|
+
attached = null;
|
|
236
|
+
dispatcherValue = null;
|
|
237
|
+
updatedFns = [];
|
|
238
|
+
actionSources = [];
|
|
239
|
+
actionReader = {
|
|
240
|
+
bound: (action) => this.states.has(action),
|
|
241
|
+
pressed: (action) => this.states.get(action)?.pressed ?? false,
|
|
242
|
+
};
|
|
243
|
+
constructor(bindings, opts = {}) {
|
|
244
|
+
this.map = bindings;
|
|
245
|
+
this.active = bindings;
|
|
246
|
+
this.getGamepads = opts.getGamepads ?? defaultGamepads;
|
|
247
|
+
this.blocked = opts.blocked ?? (() => false);
|
|
248
|
+
this.queueTaps = opts.queueTaps ?? true;
|
|
249
|
+
this.tickMs = opts.tickMs ?? 1000 / 60;
|
|
250
|
+
this.padLayout = opts.padLayout ?? null;
|
|
251
|
+
if (opts.globalKeys)
|
|
252
|
+
this.setGlobalKeys(opts.globalKeys);
|
|
253
|
+
if (opts.buttonKeys)
|
|
254
|
+
this.setButtonKeys(opts.buttonKeys);
|
|
255
|
+
this.setBindings(bindings);
|
|
256
|
+
if (opts.gestures)
|
|
257
|
+
this.setGestures(opts.gestures);
|
|
258
|
+
if (opts.pointers)
|
|
259
|
+
this.setPointers(opts.pointers);
|
|
260
|
+
if (opts.look)
|
|
261
|
+
this.setLook(opts.look);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Device that produced the most recent input (updated on real events: keys, pointer events,
|
|
265
|
+
* pad activity, XR button presses). Useful for showing the right button prompts; `on('device')`
|
|
266
|
+
* fires when it changes.
|
|
267
|
+
*/
|
|
268
|
+
get lastDevice() {
|
|
269
|
+
return this.deviceValue;
|
|
270
|
+
}
|
|
271
|
+
set lastDevice(device) {
|
|
272
|
+
if (device === this.deviceValue)
|
|
273
|
+
return;
|
|
274
|
+
this.deviceValue = device;
|
|
275
|
+
this.events.emit('device', device);
|
|
276
|
+
}
|
|
277
|
+
/** Subscribe to an `InputEvents` event; returns the unsubscriber. */
|
|
278
|
+
on(type, fn) {
|
|
279
|
+
return this.events.on(type, fn);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Link gestures (or unlink with `null`). Linked gestures share this input's `blocked()` and
|
|
283
|
+
* pointer transform, attach to the same pointer target, and advance in `update()` before the
|
|
284
|
+
* actions are read, so `{ gesture }` bindings work. Don't call `gestures.update()` yourself.
|
|
285
|
+
*/
|
|
286
|
+
setGestures(gestures) {
|
|
287
|
+
if (this.linkedGestures === gestures)
|
|
288
|
+
return;
|
|
289
|
+
if (this.linkedGestures && this.attached)
|
|
290
|
+
this.linkedGestures.detach();
|
|
291
|
+
this.linkedGestures = gestures;
|
|
292
|
+
if (!gestures)
|
|
293
|
+
return;
|
|
294
|
+
gestures.blocked = () => this.blocked();
|
|
295
|
+
gestures.setPointerTransform(this.transform);
|
|
296
|
+
this.linkedPointers?.setGestures(gestures);
|
|
297
|
+
this.wireClaim();
|
|
298
|
+
if (this.attached) {
|
|
299
|
+
gestures.attach({
|
|
300
|
+
pointerTarget: this.attached.pointerTarget,
|
|
301
|
+
disableTouchScroll: this.attached.disableTouchScroll,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/** The linked gestures (`setGestures`), or `null`. */
|
|
306
|
+
get gestures() {
|
|
307
|
+
return this.linkedGestures;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Link a pointer router (or unlink with `null`). A linked router shares this input's
|
|
311
|
+
* `blocked()`, pointer transform (client → camera screen units) and gestures (the magnify aim),
|
|
312
|
+
* attaches to the same pointer target, and is polled in `update()` before the actions are
|
|
313
|
+
* read, so `{ pointer }` and `{ xr }` bindings work; after them its `select` and `secondary`
|
|
314
|
+
* events follow the actions named by `selectAction` / `secondaryAction` when bound. Don't call
|
|
315
|
+
* `router.update()` yourself.
|
|
316
|
+
*/
|
|
317
|
+
setPointers(router) {
|
|
318
|
+
if (this.linkedPointers === router)
|
|
319
|
+
return;
|
|
320
|
+
if (this.linkedPointers && this.attached)
|
|
321
|
+
this.linkedPointers.detach();
|
|
322
|
+
this.linkedPointers = router;
|
|
323
|
+
if (!router)
|
|
324
|
+
return;
|
|
325
|
+
router.blocked = () => this.blocked();
|
|
326
|
+
router.setPointerTransform(this.transform);
|
|
327
|
+
router.setGestures(this.linkedGestures);
|
|
328
|
+
if (this.attached)
|
|
329
|
+
router.attach(this.attached.pointerTarget, this.dispatcherValue ?? undefined);
|
|
330
|
+
}
|
|
331
|
+
/** The linked pointer router (`setPointers`), or `null`. */
|
|
332
|
+
get pointers() {
|
|
333
|
+
return this.linkedPointers;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Link look controls (or unlink with `null`). Linked, they share this input's `blocked()`, read
|
|
337
|
+
* their move keys from its raw key state, attach to the same pointer target and advance in
|
|
338
|
+
* `update()`, so `look.look` and `look.move` are this tick's. Don't call `look.update()` yourself.
|
|
339
|
+
*/
|
|
340
|
+
setLook(look) {
|
|
341
|
+
if (this.linkedLook === look)
|
|
342
|
+
return;
|
|
343
|
+
if (this.linkedLook) {
|
|
344
|
+
if (this.attached)
|
|
345
|
+
this.linkedLook.detach();
|
|
346
|
+
this.linkedLook.keyHeld = null;
|
|
347
|
+
this.linkedLook.orbitFromInput = false;
|
|
348
|
+
this.linkedLook.pointerTransform = null;
|
|
349
|
+
this.linkedLook.padsFromInput = null;
|
|
350
|
+
}
|
|
351
|
+
this.linkedLook = look;
|
|
352
|
+
this.wireClaim();
|
|
353
|
+
if (!look)
|
|
354
|
+
return;
|
|
355
|
+
look.blocked = () => this.blocked();
|
|
356
|
+
look.keyHeld = (code) => this.keysDown.has(code);
|
|
357
|
+
// Orbit drags come through this input's own pointer feed, so recordings capture them.
|
|
358
|
+
look.orbitFromInput = true;
|
|
359
|
+
look.pointerTransform = this.transform;
|
|
360
|
+
// Its pad stick reads this tick's pads, recorded and replayed with the rest (ws/input-9).
|
|
361
|
+
look.padsFromInput = this.readTickPads;
|
|
362
|
+
if (this.attached)
|
|
363
|
+
look.attach(this.attached.pointerTarget, this.dispatcherValue ?? undefined);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* The pointer dispatcher `attach` built (one DOM listener set shared by the feed, gestures,
|
|
367
|
+
* look, router and touch zones), or `null` while detached.
|
|
368
|
+
*/
|
|
369
|
+
get dispatcher() {
|
|
370
|
+
return this.dispatcherValue;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Run `fn` at the end of every `update()`, after gestures, look, router and triggers have
|
|
374
|
+
* advanced (same tick, registration order): the place to poll a world panel or read
|
|
375
|
+
* `pressed()` for the frame. Returns the remover.
|
|
376
|
+
*/
|
|
377
|
+
onUpdated(fn) {
|
|
378
|
+
this.updatedFns.push(fn);
|
|
379
|
+
return () => {
|
|
380
|
+
const i = this.updatedFns.indexOf(fn);
|
|
381
|
+
if (i >= 0)
|
|
382
|
+
this.updatedFns.splice(i, 1);
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/** Add an `ActionSource` (touch zones). Returns the remover. */
|
|
386
|
+
addActionSource(src) {
|
|
387
|
+
this.actionSources.push(src);
|
|
388
|
+
return () => {
|
|
389
|
+
const i = this.actionSources.indexOf(src);
|
|
390
|
+
if (i >= 0)
|
|
391
|
+
this.actionSources.splice(i, 1);
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/** `value(positive) - value(negative)`, -1..1: a move axis from two actions. */
|
|
395
|
+
axis(negative, positive) {
|
|
396
|
+
return this.value(positive) - this.value(negative);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Gives the linked gestures the look's pointer claim (an orbit drag is never a pan) when the
|
|
400
|
+
* gestures have no claim of their own; takes it back when the look unlinks.
|
|
401
|
+
*/
|
|
402
|
+
wireClaim() {
|
|
403
|
+
const g = this.linkedGestures;
|
|
404
|
+
if (!g)
|
|
405
|
+
return;
|
|
406
|
+
if (this.linkedLook) {
|
|
407
|
+
if (!g.claim || g.claim === this.claimFn)
|
|
408
|
+
g.claim = this.claimFn;
|
|
409
|
+
}
|
|
410
|
+
else if (g.claim === this.claimFn)
|
|
411
|
+
g.claim = null;
|
|
412
|
+
}
|
|
413
|
+
/** The linked look controls (`setLook`), or `null`. */
|
|
414
|
+
get look() {
|
|
415
|
+
return this.linkedLook;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Replace the base bindings (below every pushed context). Action states and the input buffer
|
|
419
|
+
* reset (use `rebind` to change one action and keep the rest). The map may leave out actions
|
|
420
|
+
* of `A` (ws/input-12): a pushed context binds those.
|
|
421
|
+
*/
|
|
422
|
+
setBindings(bindings) {
|
|
423
|
+
this.map = bindings;
|
|
424
|
+
this.states.clear();
|
|
425
|
+
this.triggers.clear();
|
|
426
|
+
this.presses.clear();
|
|
427
|
+
this.keyRepeats.clear();
|
|
428
|
+
this.buttonRepeats.clear();
|
|
429
|
+
for (const action of Object.keys(bindings)) {
|
|
430
|
+
this.states.set(action, {
|
|
431
|
+
pressed: false,
|
|
432
|
+
held: false,
|
|
433
|
+
released: false,
|
|
434
|
+
value: 0,
|
|
435
|
+
heldTicks: 0,
|
|
436
|
+
holdProgress: 0,
|
|
437
|
+
});
|
|
438
|
+
this.presses.set(action, { tick: -1, used: false });
|
|
439
|
+
}
|
|
440
|
+
this.extraActions = false;
|
|
441
|
+
for (const c of this.stack)
|
|
442
|
+
this.addContextActions(c.bindings);
|
|
443
|
+
this.active = this.resolveContexts();
|
|
444
|
+
for (const action of this.states.keys()) {
|
|
445
|
+
this.triggers.set(action, this.active[action].map(newTriggerState));
|
|
446
|
+
}
|
|
447
|
+
this.rebuildBound();
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Replace one action's bindings, keeping every other action's state (ws/input-3). Rebinding
|
|
451
|
+
* while an input is held is safe: the old action sees `released` next tick, and a new binding
|
|
452
|
+
* whose input is already down reads inactive until it lifts and goes down again.
|
|
453
|
+
*
|
|
454
|
+
* With contexts pushed (ws/input-5) it writes into the top context (the action becomes one that
|
|
455
|
+
* context binds); `context` names another pushed context instead, `null` the base map.
|
|
456
|
+
*/
|
|
457
|
+
rebind(action, list, context) {
|
|
458
|
+
if (!this.states.has(action))
|
|
459
|
+
throw new Error(`unknown action "${action}"`);
|
|
460
|
+
const entry = this.contextEntry(context);
|
|
461
|
+
if (entry)
|
|
462
|
+
entry.bindings = { ...entry.bindings, [action]: list };
|
|
463
|
+
else
|
|
464
|
+
this.map = { ...this.map, [action]: list };
|
|
465
|
+
// The action's trigger state is always rebuilt (latched), as before contexts.
|
|
466
|
+
this.applyActive(action);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* The bindings in force (what `serializeBindings` saves): the base map with the pushed
|
|
470
|
+
* contexts resolved over it. `context` reads one context's own bindings over the ones below
|
|
471
|
+
* it as if it were on top (`null`: the base map).
|
|
472
|
+
*/
|
|
473
|
+
bindings(context) {
|
|
474
|
+
if (context === undefined)
|
|
475
|
+
return this.active;
|
|
476
|
+
if (context === null)
|
|
477
|
+
return this.map;
|
|
478
|
+
const i = this.contextIndex(context);
|
|
479
|
+
if (i < 0)
|
|
480
|
+
throw new Error(`no context "${context}"`);
|
|
481
|
+
return this.resolveContexts(i);
|
|
482
|
+
}
|
|
483
|
+
// ---------------------------------------------------------------- contexts
|
|
484
|
+
/**
|
|
485
|
+
* Push an action context (ws/input-5): a menu, gameplay, a vehicle, each with its own bindings.
|
|
486
|
+
* The top context's bindings win; `pass` says what happens to the actions it does not bind
|
|
487
|
+
* (default `'unbound'`: they fall through). Pushing a name already on the stack moves it to the
|
|
488
|
+
* top with the new options. Every action whose bindings in force change resets its hold
|
|
489
|
+
* progress, tap counts and buffered press, and a binding whose input is already down reads
|
|
490
|
+
* inactive until it lifts, so a held key never fires across the change. Returns the remover
|
|
491
|
+
* (it removes this push only).
|
|
492
|
+
*/
|
|
493
|
+
pushContext(name, opts = {}) {
|
|
494
|
+
const bindings = (opts.bindings ?? {});
|
|
495
|
+
this.addContextActions(bindings);
|
|
496
|
+
const old = this.contextIndex(name);
|
|
497
|
+
if (old >= 0)
|
|
498
|
+
this.stack.splice(old, 1);
|
|
499
|
+
const entry = { name, bindings, pass: opts.pass ?? 'unbound' };
|
|
500
|
+
this.stack.push(entry);
|
|
501
|
+
this.contextNames = this.stack.map((c) => c.name);
|
|
502
|
+
const hooks = this.feedHooks;
|
|
503
|
+
for (let i = 0; i < hooks.length; i++)
|
|
504
|
+
hooks[i].context?.(name, true);
|
|
505
|
+
this.applyActive(null);
|
|
506
|
+
this.events.emit('context', name);
|
|
507
|
+
return () => {
|
|
508
|
+
const i = this.stack.indexOf(entry);
|
|
509
|
+
if (i >= 0)
|
|
510
|
+
this.removeContextAt(i);
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/** Remove the topmost context named `name`. Returns false when none is pushed. */
|
|
514
|
+
popContext(name) {
|
|
515
|
+
const i = this.contextIndex(name);
|
|
516
|
+
if (i < 0)
|
|
517
|
+
return false;
|
|
518
|
+
this.removeContextAt(i);
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
/** The top context's name, `null` with none pushed. */
|
|
522
|
+
get context() {
|
|
523
|
+
return this.stack[this.stack.length - 1]?.name ?? null;
|
|
524
|
+
}
|
|
525
|
+
/** The pushed context names, bottom first. One array, rebuilt when the stack changes. */
|
|
526
|
+
get contexts() {
|
|
527
|
+
return this.contextNames;
|
|
528
|
+
}
|
|
529
|
+
/** Shorthand for `on('context', fn)`: the top context changed. Returns the remover. */
|
|
530
|
+
onContextChange(fn) {
|
|
531
|
+
return this.events.on('context', fn);
|
|
532
|
+
}
|
|
533
|
+
removeContextAt(i) {
|
|
534
|
+
const before = this.context;
|
|
535
|
+
const [entry] = this.stack.splice(i, 1);
|
|
536
|
+
this.contextNames = this.stack.map((c) => c.name);
|
|
537
|
+
const hooks = this.feedHooks;
|
|
538
|
+
for (let h = 0; h < hooks.length; h++)
|
|
539
|
+
hooks[h].context?.(entry.name, false);
|
|
540
|
+
this.applyActive(null);
|
|
541
|
+
const now = this.context;
|
|
542
|
+
if (now !== before)
|
|
543
|
+
this.events.emit('context', now);
|
|
544
|
+
}
|
|
545
|
+
contextIndex(name) {
|
|
546
|
+
for (let i = this.stack.length - 1; i >= 0; i--) {
|
|
547
|
+
if (this.stack[i].name === name)
|
|
548
|
+
return i;
|
|
549
|
+
}
|
|
550
|
+
return -1;
|
|
551
|
+
}
|
|
552
|
+
/** The context `rebind` writes to: `undefined` the top one (or none), `null` none, else by name. */
|
|
553
|
+
contextEntry(context) {
|
|
554
|
+
if (context === null)
|
|
555
|
+
return null;
|
|
556
|
+
if (context === undefined)
|
|
557
|
+
return this.stack[this.stack.length - 1] ?? null;
|
|
558
|
+
const i = this.contextIndex(context);
|
|
559
|
+
if (i < 0)
|
|
560
|
+
throw new Error(`no context "${context}"`);
|
|
561
|
+
return this.stack[i];
|
|
562
|
+
}
|
|
563
|
+
/** Give a context's actions the base map lacks a state (bound to nothing below the context). */
|
|
564
|
+
addContextActions(bindings) {
|
|
565
|
+
for (const action of Object.keys(bindings)) {
|
|
566
|
+
if (this.states.has(action))
|
|
567
|
+
continue;
|
|
568
|
+
this.states.set(action, {
|
|
569
|
+
pressed: false,
|
|
570
|
+
held: false,
|
|
571
|
+
released: false,
|
|
572
|
+
value: 0,
|
|
573
|
+
heldTicks: 0,
|
|
574
|
+
holdProgress: 0,
|
|
575
|
+
});
|
|
576
|
+
this.presses.set(action, { tick: -1, used: false });
|
|
577
|
+
this.triggers.set(action, []);
|
|
578
|
+
this.extraActions = true;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/** The bindings in force with contexts `0..top` on the stack (all of them by default). */
|
|
582
|
+
resolveContexts(top = this.stack.length - 1) {
|
|
583
|
+
if (top < 0 && !this.extraActions)
|
|
584
|
+
return this.map;
|
|
585
|
+
const out = {};
|
|
586
|
+
for (const action of this.states.keys())
|
|
587
|
+
out[action] = this.resolveAction(action, top);
|
|
588
|
+
return out;
|
|
589
|
+
}
|
|
590
|
+
resolveAction(action, i) {
|
|
591
|
+
if (i < 0)
|
|
592
|
+
return this.map[action] ?? EMPTY_BINDINGS;
|
|
593
|
+
const c = this.stack[i];
|
|
594
|
+
const own = c.bindings[action];
|
|
595
|
+
if (own)
|
|
596
|
+
return c.pass === 'all' ? [...own, ...this.resolveAction(action, i - 1)] : own;
|
|
597
|
+
return c.pass === 'none' ? EMPTY_BINDINGS : this.resolveAction(action, i - 1);
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Recompute the bindings in force; every action whose list changed (element by element), and
|
|
601
|
+
* `force` too, gets fresh trigger state, latched where its input is down, and loses its
|
|
602
|
+
* buffered press.
|
|
603
|
+
*/
|
|
604
|
+
applyActive(force) {
|
|
605
|
+
const prev = this.active;
|
|
606
|
+
const next = this.resolveContexts();
|
|
607
|
+
this.active = next;
|
|
608
|
+
for (const action of this.states.keys()) {
|
|
609
|
+
const list = next[action];
|
|
610
|
+
if (action !== force && sameList(prev[action], list))
|
|
611
|
+
continue;
|
|
612
|
+
const ts = list.map(newTriggerState);
|
|
613
|
+
for (let i = 0; i < list.length; i++) {
|
|
614
|
+
if (this.rawValue(list[i]) > 0)
|
|
615
|
+
ts[i].latched = true;
|
|
616
|
+
}
|
|
617
|
+
this.triggers.set(action, ts);
|
|
618
|
+
if (action !== force) {
|
|
619
|
+
const b = this.presses.get(action);
|
|
620
|
+
if (b)
|
|
621
|
+
b.tick = -1;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
this.rebuildBound();
|
|
625
|
+
}
|
|
626
|
+
// ---------------------------------------------------------------- buffer
|
|
627
|
+
/**
|
|
628
|
+
* True if `action` went `pressed` within the last `windowMs` (counted in ticks:
|
|
629
|
+
* `ceil(windowMs / tickMs)`, this tick included) and that press was not `consume`d
|
|
630
|
+
* (ws/input-5). A press a few frames early still lands: `if (input.buffered('jump', 100) &&
|
|
631
|
+
* grounded) { input.consume('jump'); jump(); }`. Deterministic under replay.
|
|
632
|
+
*/
|
|
633
|
+
buffered(action, windowMs) {
|
|
634
|
+
const b = this.presses.get(action);
|
|
635
|
+
if (!b || b.tick < 0 || b.used)
|
|
636
|
+
return false;
|
|
637
|
+
return this.tick - b.tick <= Math.ceil(windowMs / this.tickMs);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Mark `action`'s latest press used: `buffered` no longer returns it and `pressed` reads false
|
|
641
|
+
* for the rest of this tick. Returns whether there was an unused press. Feed hooks see it, so a
|
|
642
|
+
* recording carries it.
|
|
643
|
+
*/
|
|
644
|
+
consume(action) {
|
|
645
|
+
const b = this.presses.get(action);
|
|
646
|
+
if (!b)
|
|
647
|
+
return false;
|
|
648
|
+
const hooks = this.feedHooks;
|
|
649
|
+
for (let i = 0; i < hooks.length; i++)
|
|
650
|
+
hooks[i].consume?.(action);
|
|
651
|
+
const st = this.states.get(action);
|
|
652
|
+
st.pressed = false;
|
|
653
|
+
if (b.tick < 0 || b.used)
|
|
654
|
+
return false;
|
|
655
|
+
b.used = true;
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
// ---------------------------------------------------------------- sticks
|
|
659
|
+
/**
|
|
660
|
+
* Read four actions as a stick into `out` (`[x, y]`, the caller's; returned) through
|
|
661
|
+
* `shapeStick` (ws/input-5): x is `value(posX) - value(negX)`, y `value(posY) - value(negY)`.
|
|
662
|
+
* Keys, pad axes (bind them with `threshold: 0` for an analog read) and touch-zone sticks all
|
|
663
|
+
* go through it; diagonal keys read length 1. Allocation-free.
|
|
664
|
+
*/
|
|
665
|
+
stick(negX, posX, negY, posY, out, opts) {
|
|
666
|
+
const x = this.value(posX) - this.value(negX);
|
|
667
|
+
const y = this.value(posY) - this.value(negY);
|
|
668
|
+
return shapeStick(x, y, out, opts);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* The aim angle of a stick read (`Math.atan2(y, x)`, radians), `null` when it is at rest
|
|
672
|
+
* (inside the deadzone). Pass the array `stick` filled.
|
|
673
|
+
*/
|
|
674
|
+
stickAngle(out) {
|
|
675
|
+
if (out[0] === 0 && out[1] === 0)
|
|
676
|
+
return null;
|
|
677
|
+
return Math.atan2(out[1], out[0]);
|
|
678
|
+
}
|
|
679
|
+
rebuildBound() {
|
|
680
|
+
this.boundKeys = new Set();
|
|
681
|
+
this.boundMouse = new Set();
|
|
682
|
+
this.boundWheel = false;
|
|
683
|
+
for (const action of this.states.keys()) {
|
|
684
|
+
for (const b of this.active[action])
|
|
685
|
+
this.noteBound(b);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
noteBound(b) {
|
|
689
|
+
if ('key' in b)
|
|
690
|
+
this.boundKeys.add(b.key);
|
|
691
|
+
else if ('mouse' in b)
|
|
692
|
+
this.boundMouse.add(b.mouse);
|
|
693
|
+
else if ('wheel' in b)
|
|
694
|
+
this.boundWheel = true;
|
|
695
|
+
else if ('chord' in b)
|
|
696
|
+
for (const m of b.chord)
|
|
697
|
+
this.noteBound(m);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Resolve with the next input the player presses (ws/input-3): a key as `{ key }`, a mouse
|
|
701
|
+
* or pen button as `{ mouse }`, a pad button that was up when the listen began as
|
|
702
|
+
* `{ padButton }`, an XR button as `{ xr }`. Escape, a `timeoutMs` elapsing or a newer
|
|
703
|
+
* `listen` resolve `null`. The captured input is swallowed (no action sees it). Keys typed
|
|
704
|
+
* into text fields never reach `Input`, so they are not captured either. The caller rebinds:
|
|
705
|
+
* `const b = await input.listen('jump'); if (b) input.rebind('jump', [b]);`.
|
|
706
|
+
*/
|
|
707
|
+
listen(action, opts = {}) {
|
|
708
|
+
this.listenState?.resolve(null);
|
|
709
|
+
return new Promise((resolve) => {
|
|
710
|
+
const baseline = new Set();
|
|
711
|
+
this.pads.forEach((p, pi) => {
|
|
712
|
+
if (!p || p.connected === false)
|
|
713
|
+
return;
|
|
714
|
+
p.buttons.forEach((b, bi) => {
|
|
715
|
+
if (b.pressed)
|
|
716
|
+
baseline.add(pi * 64 + bi);
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
const ticks = opts.timeoutMs === undefined ? Infinity : Math.ceil(opts.timeoutMs / this.tickMs);
|
|
720
|
+
this.listenState = { action, resolve, deadline: this.tick + ticks, baseline };
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
/** The action a `listen` is waiting for, or `null`. */
|
|
724
|
+
get listening() {
|
|
725
|
+
return this.listenState?.action ?? null;
|
|
726
|
+
}
|
|
727
|
+
settleListen(b) {
|
|
728
|
+
const l = this.listenState;
|
|
729
|
+
if (!l)
|
|
730
|
+
return;
|
|
731
|
+
this.listenState = null;
|
|
732
|
+
l.resolve(b);
|
|
733
|
+
}
|
|
734
|
+
/** Replace the global keys (see `InputOptions.globalKeys`). */
|
|
735
|
+
setGlobalKeys(codes) {
|
|
736
|
+
this.globalKeys = new Set(codes);
|
|
737
|
+
}
|
|
738
|
+
/** Replace the button keys (see `InputOptions.buttonKeys`). */
|
|
739
|
+
setButtonKeys(codes) {
|
|
740
|
+
this.buttonKeys = new Set(codes);
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* The prompt glyph for `action` on `device` (default `lastDevice`): the first binding that
|
|
744
|
+
* device produces, else the action's first binding; `null` for an unbound action. Pad labels
|
|
745
|
+
* follow `padLayout`, guessed from the first connected pad's `id` when unset. Reads the
|
|
746
|
+
* bindings in force, so the top context's (ws/input-5).
|
|
747
|
+
*/
|
|
748
|
+
glyphFor(action, device = this.deviceValue) {
|
|
749
|
+
const list = this.active[action];
|
|
750
|
+
if (!list || list.length === 0)
|
|
751
|
+
return null;
|
|
752
|
+
const b = list.find((x) => bindingMatchesDevice(x, device)) ?? list[0];
|
|
753
|
+
return glyphForBinding(b, this.padLayout ?? guessPadLayout(this.firstPad()?.id));
|
|
754
|
+
}
|
|
755
|
+
firstPad(index) {
|
|
756
|
+
if (index !== undefined) {
|
|
757
|
+
const p = this.pads[index];
|
|
758
|
+
return p && p.connected !== false ? p : null;
|
|
759
|
+
}
|
|
760
|
+
for (const p of this.pads)
|
|
761
|
+
if (p && p.connected !== false)
|
|
762
|
+
return p;
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
/** True when `pad` (or any connected pad) has a vibration actuator `rumble` can drive. */
|
|
766
|
+
rumbleSupported(pad) {
|
|
767
|
+
return this.actuatorPad(pad) !== null;
|
|
768
|
+
}
|
|
769
|
+
actuatorPad(index) {
|
|
770
|
+
if (index !== undefined) {
|
|
771
|
+
const p = this.firstPad(index);
|
|
772
|
+
return p?.vibrationActuator ? p : null;
|
|
773
|
+
}
|
|
774
|
+
for (const p of this.pads)
|
|
775
|
+
if (p && p.connected !== false && p.vibrationActuator)
|
|
776
|
+
return p;
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Play a dual-rumble effect on `pad` (default: the first connected pad that can). Returns
|
|
781
|
+
* true when an actuator took it; a no-op (false) otherwise, so games call it freely. Feed hooks
|
|
782
|
+
* see every call, so a recording carries the rumbles for playback to assert.
|
|
783
|
+
*/
|
|
784
|
+
rumble(opts = {}) {
|
|
785
|
+
const strong = clamp01(opts.strong ?? 1);
|
|
786
|
+
const weak = clamp01(opts.weak ?? 1);
|
|
787
|
+
const ms = Math.max(0, opts.ms ?? 100);
|
|
788
|
+
const hooks = this.feedHooks;
|
|
789
|
+
for (let i = 0; i < hooks.length; i++)
|
|
790
|
+
hooks[i].rumble?.(strong, weak, ms, opts.pad);
|
|
791
|
+
this.events.emit('rumble', opts);
|
|
792
|
+
const p = this.actuatorPad(opts.pad);
|
|
793
|
+
if (!p?.vibrationActuator)
|
|
794
|
+
return false;
|
|
795
|
+
try {
|
|
796
|
+
const r = p.vibrationActuator.playEffect('dual-rumble', {
|
|
797
|
+
duration: ms,
|
|
798
|
+
startDelay: 0,
|
|
799
|
+
strongMagnitude: strong,
|
|
800
|
+
weakMagnitude: weak,
|
|
801
|
+
});
|
|
802
|
+
r?.catch?.(() => { });
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Install a feed hook: it sees every `keyDown`, `keyUp`, `buttonDown`, `buttonUp`, `wheel`,
|
|
811
|
+
* `pointerAt` and `releaseAll` call (DOM or direct) and the pads read in `update()`, and may
|
|
812
|
+
* substitute those pads. Returns the uninstaller. Used by `createInputRecorder` and
|
|
813
|
+
* `createInputPlayback`; hooks never change how bindings are evaluated.
|
|
814
|
+
*/
|
|
815
|
+
onFeed(hook) {
|
|
816
|
+
this.feedHooks.push(hook);
|
|
817
|
+
return () => {
|
|
818
|
+
const i = this.feedHooks.indexOf(hook);
|
|
819
|
+
if (i >= 0)
|
|
820
|
+
this.feedHooks.splice(i, 1);
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/** Install the client → game-space mapping (platform owns letterboxing). `null` = identity relative to the target. */
|
|
824
|
+
setPointerTransform(fn) {
|
|
825
|
+
this.transform = fn;
|
|
826
|
+
this.linkedGestures?.setPointerTransform(fn);
|
|
827
|
+
this.linkedPointers?.setPointerTransform(fn);
|
|
828
|
+
this.dispatcherValue?.setTransform(fn);
|
|
829
|
+
if (this.linkedLook)
|
|
830
|
+
this.linkedLook.pointerTransform = fn;
|
|
831
|
+
}
|
|
832
|
+
// ---------------------------------------------------------------- queries
|
|
833
|
+
pressed(action) {
|
|
834
|
+
return this.states.get(action)?.pressed ?? false;
|
|
835
|
+
}
|
|
836
|
+
held(action) {
|
|
837
|
+
return this.states.get(action)?.held ?? false;
|
|
838
|
+
}
|
|
839
|
+
released(action) {
|
|
840
|
+
return this.states.get(action)?.released ?? false;
|
|
841
|
+
}
|
|
842
|
+
value(action) {
|
|
843
|
+
return this.states.get(action)?.value ?? 0;
|
|
844
|
+
}
|
|
845
|
+
state(action) {
|
|
846
|
+
const st = this.states.get(action);
|
|
847
|
+
if (!st)
|
|
848
|
+
throw new Error(`unknown action "${action}"`);
|
|
849
|
+
return st;
|
|
850
|
+
}
|
|
851
|
+
/** True if any bound action was pressed this tick. */
|
|
852
|
+
anyPressed() {
|
|
853
|
+
for (const st of this.states.values())
|
|
854
|
+
if (st.pressed)
|
|
855
|
+
return true;
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
/** Raw key state by `KeyboardEvent.code`, ignoring bindings and blocking. */
|
|
859
|
+
keyHeld(code) {
|
|
860
|
+
return this.keysDown.has(code);
|
|
861
|
+
}
|
|
862
|
+
// ---------------------------------------------------------------- tick
|
|
863
|
+
/** Advance one tick: poll gamepads, compute edges, clear per-tick accumulators. */
|
|
864
|
+
update() {
|
|
865
|
+
// The pads are read first, so a linked look turns from the same snapshot the action map
|
|
866
|
+
// reads and a recorder writes down (ws/input-9), and a playback's replace both.
|
|
867
|
+
let pads = this.getGamepads();
|
|
868
|
+
const hooks = this.feedHooks;
|
|
869
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
870
|
+
const src = hooks[i].padSource;
|
|
871
|
+
if (src)
|
|
872
|
+
pads = src(pads);
|
|
873
|
+
}
|
|
874
|
+
this.pads = pads;
|
|
875
|
+
this.linkedGestures?.update();
|
|
876
|
+
this.linkedLook?.update();
|
|
877
|
+
this.linkedPointers?.poll();
|
|
878
|
+
const blocked = this.blocked();
|
|
879
|
+
for (let i = 0; i < hooks.length; i++)
|
|
880
|
+
pads = hooks[i].gamepads(pads);
|
|
881
|
+
this.pads = pads;
|
|
882
|
+
this.tick++;
|
|
883
|
+
let padActive = false;
|
|
884
|
+
if (this.listenState)
|
|
885
|
+
this.pollListen();
|
|
886
|
+
const sources = this.actionSources;
|
|
887
|
+
for (let i = 0; i < sources.length; i++)
|
|
888
|
+
sources[i].update?.();
|
|
889
|
+
for (const [action, st] of this.states) {
|
|
890
|
+
const v = this.evaluate(action, this.active[action], this.triggers.get(action));
|
|
891
|
+
if (v.pad)
|
|
892
|
+
padActive = true;
|
|
893
|
+
const active = v.value > 0;
|
|
894
|
+
if (blocked) {
|
|
895
|
+
// Report nothing. A key held through the block must not fire `pressed` afterwards,
|
|
896
|
+
// so remember it as held internally via heldTicks > 0.
|
|
897
|
+
st.pressed = false;
|
|
898
|
+
st.released = false;
|
|
899
|
+
st.value = 0;
|
|
900
|
+
st.held = false;
|
|
901
|
+
st.holdProgress = 0;
|
|
902
|
+
st.heldTicks = active ? Math.max(1, st.heldTicks) : 0;
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
const was = st.heldTicks > 0;
|
|
906
|
+
st.pressed = active && (!was || v.fresh);
|
|
907
|
+
st.released = !active && was && st.held;
|
|
908
|
+
st.held = active;
|
|
909
|
+
st.value = v.value;
|
|
910
|
+
st.holdProgress = v.hold;
|
|
911
|
+
st.heldTicks = active ? st.heldTicks + 1 : 0;
|
|
912
|
+
if (st.pressed) {
|
|
913
|
+
const b = this.presses.get(action);
|
|
914
|
+
b.tick = this.tick;
|
|
915
|
+
b.used = false;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (padActive)
|
|
919
|
+
this.lastDevice = 'gamepad';
|
|
920
|
+
const r = this.linkedPointers;
|
|
921
|
+
if (r && (r.xrFired('trigger') || r.xrFired('squeeze') || r.xrFired('pinch'))) {
|
|
922
|
+
this.lastDevice = 'xr';
|
|
923
|
+
}
|
|
924
|
+
if (blocked) {
|
|
925
|
+
this.keyRepeats.clear();
|
|
926
|
+
this.buttonRepeats.clear();
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
consumeRepeats(this.keyRepeats, this.keysTapped);
|
|
930
|
+
consumeRepeats(this.buttonRepeats, this.buttonsTapped);
|
|
931
|
+
}
|
|
932
|
+
rememberActive(this.prevKeys, this.keysDown, this.keysTapped);
|
|
933
|
+
rememberActive(this.prevButtons, this.buttonsDown, this.buttonsTapped);
|
|
934
|
+
this.keysTapped.clear();
|
|
935
|
+
this.buttonsTapped.clear();
|
|
936
|
+
this.pointer.wheelX = this.wheelAccX;
|
|
937
|
+
this.pointer.wheelY = this.wheelAccY;
|
|
938
|
+
this.wheelAccX = 0;
|
|
939
|
+
this.wheelAccY = 0;
|
|
940
|
+
this.pointer.dx = this.pointer.x - this.prevX;
|
|
941
|
+
this.pointer.dy = this.pointer.y - this.prevY;
|
|
942
|
+
this.prevX = this.pointer.x;
|
|
943
|
+
this.prevY = this.pointer.y;
|
|
944
|
+
this.linkedPointers?.resolve(this.actionReader);
|
|
945
|
+
const fns = this.updatedFns;
|
|
946
|
+
for (let i = 0; i < fns.length; i++)
|
|
947
|
+
fns[i]();
|
|
948
|
+
}
|
|
949
|
+
/** A pending `listen`: pad buttons newly down, XR presses, the timeout. Keys and mouse come via the feed. */
|
|
950
|
+
pollListen() {
|
|
951
|
+
const l = this.listenState;
|
|
952
|
+
if (this.tick > l.deadline) {
|
|
953
|
+
this.settleListen(null);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
for (let pi = 0; pi < this.pads.length; pi++) {
|
|
957
|
+
const p = this.pads[pi];
|
|
958
|
+
if (!p || p.connected === false)
|
|
959
|
+
continue;
|
|
960
|
+
for (let bi = 0; bi < p.buttons.length; bi++) {
|
|
961
|
+
if (p.buttons[bi].pressed && !l.baseline.has(pi * 64 + bi)) {
|
|
962
|
+
this.lastDevice = 'gamepad';
|
|
963
|
+
this.settleListen({ padButton: bi });
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const r = this.linkedPointers;
|
|
969
|
+
if (r) {
|
|
970
|
+
for (const xr of XR_BUTTONS) {
|
|
971
|
+
if (r.xrFired(xr)) {
|
|
972
|
+
this.lastDevice = 'xr';
|
|
973
|
+
this.settleListen({ xr });
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
evalOut = { value: 0, pad: false, fresh: false, hold: 0 };
|
|
980
|
+
/**
|
|
981
|
+
* Fold a list of bindings into one value: the largest raw value after each binding's trigger
|
|
982
|
+
* (`press` passes it through, `hold` gates it on `holdMs` of ticks down, `doubleTap` passes a
|
|
983
|
+
* second down edge within `doubleTapMs`). `fresh` marks a new press while already active,
|
|
984
|
+
* `hold` the furthest hold progress, `pad` that a pad produced the value.
|
|
985
|
+
*/
|
|
986
|
+
evaluate(action, list, ts) {
|
|
987
|
+
const out = this.evalOut;
|
|
988
|
+
out.value = 0;
|
|
989
|
+
out.pad = false;
|
|
990
|
+
out.fresh = false;
|
|
991
|
+
out.hold = 0;
|
|
992
|
+
const sources = this.actionSources;
|
|
993
|
+
for (let i = 0; i < sources.length; i++) {
|
|
994
|
+
const src = sources[i];
|
|
995
|
+
const v = src.value(action);
|
|
996
|
+
if (v > out.value)
|
|
997
|
+
out.value = v;
|
|
998
|
+
if (v > 0 && src.fresh?.(action))
|
|
999
|
+
out.fresh = true;
|
|
1000
|
+
}
|
|
1001
|
+
for (let i = 0; i < list.length; i++) {
|
|
1002
|
+
const b = list[i];
|
|
1003
|
+
const t = ts[i];
|
|
1004
|
+
const freshBefore = out.fresh;
|
|
1005
|
+
out.fresh = false;
|
|
1006
|
+
let v = this.rawValue(b);
|
|
1007
|
+
const rawFresh = out.fresh;
|
|
1008
|
+
out.fresh = freshBefore;
|
|
1009
|
+
const down = v > 0;
|
|
1010
|
+
const edge = down && (!t.wasDown || rawFresh);
|
|
1011
|
+
t.wasDown = down;
|
|
1012
|
+
if (t.latched) {
|
|
1013
|
+
// Installed by `rebind` while its input was down: silent until a fresh press.
|
|
1014
|
+
if (!down)
|
|
1015
|
+
t.latched = false;
|
|
1016
|
+
t.downTicks = 0;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (b.trigger === undefined || b.trigger === 'press') {
|
|
1020
|
+
if (rawFresh)
|
|
1021
|
+
out.fresh = true;
|
|
1022
|
+
}
|
|
1023
|
+
else if (b.trigger === 'hold') {
|
|
1024
|
+
const need = Math.max(1, Math.ceil((b.holdMs ?? DEFAULT_HOLD_MS) / this.tickMs));
|
|
1025
|
+
t.downTicks = down ? (edge ? 1 : t.downTicks + 1) : 0;
|
|
1026
|
+
const progress = Math.min(1, t.downTicks / need);
|
|
1027
|
+
if (progress > out.hold)
|
|
1028
|
+
out.hold = progress;
|
|
1029
|
+
if (t.downTicks < need)
|
|
1030
|
+
v = 0;
|
|
1031
|
+
else if (t.downTicks === need)
|
|
1032
|
+
out.fresh = true;
|
|
1033
|
+
}
|
|
1034
|
+
else if (b.trigger === 'doubleTap') {
|
|
1035
|
+
const window = Math.max(1, Math.ceil((b.doubleTapMs ?? DEFAULT_DOUBLE_TAP_MS) / this.tickMs));
|
|
1036
|
+
if (edge && t.lastDownTick >= 0 && this.tick - t.lastDownTick <= window) {
|
|
1037
|
+
t.lastDownTick = -1;
|
|
1038
|
+
out.fresh = true;
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
if (edge)
|
|
1042
|
+
t.lastDownTick = this.tick;
|
|
1043
|
+
v = 0;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (v > out.value)
|
|
1047
|
+
out.value = v;
|
|
1048
|
+
}
|
|
1049
|
+
return out;
|
|
1050
|
+
}
|
|
1051
|
+
/** One binding's raw value before its trigger; sets `evalOut.fresh`/`pad` as a side effect. */
|
|
1052
|
+
rawValue(b) {
|
|
1053
|
+
const out = this.evalOut;
|
|
1054
|
+
let v = 0;
|
|
1055
|
+
if ('key' in b) {
|
|
1056
|
+
const tapped = this.keysTapped.has(b.key);
|
|
1057
|
+
// A queued repeat counts once the tick holding its first tap has passed.
|
|
1058
|
+
const queued = !tapped && (this.keyRepeats.get(b.key) ?? 0) > 0;
|
|
1059
|
+
// Released and pressed again since last tick: a new press, not a hold.
|
|
1060
|
+
if (queued || (tapped && this.prevKeys.has(b.key)))
|
|
1061
|
+
out.fresh = true;
|
|
1062
|
+
v = this.keysDown.has(b.key) || tapped || queued ? 1 : 0;
|
|
1063
|
+
}
|
|
1064
|
+
else if ('mouse' in b) {
|
|
1065
|
+
const tapped = this.buttonsTapped.has(b.mouse);
|
|
1066
|
+
const queued = !tapped && (this.buttonRepeats.get(b.mouse) ?? 0) > 0;
|
|
1067
|
+
if (queued || (tapped && this.prevButtons.has(b.mouse)))
|
|
1068
|
+
out.fresh = true;
|
|
1069
|
+
v = this.buttonsDown.has(b.mouse) || tapped || queued ? 1 : 0;
|
|
1070
|
+
}
|
|
1071
|
+
else if ('wheel' in b) {
|
|
1072
|
+
v = Math.sign(this.wheelAccY) === b.wheel ? 1 : 0;
|
|
1073
|
+
}
|
|
1074
|
+
else if ('padButton' in b) {
|
|
1075
|
+
v = this.padValue(b.pad, (p) => {
|
|
1076
|
+
const btn = p.buttons[b.padButton];
|
|
1077
|
+
if (!btn)
|
|
1078
|
+
return 0;
|
|
1079
|
+
const t = b.threshold ?? DEFAULT_BUTTON_THRESHOLD;
|
|
1080
|
+
return btn.pressed || btn.value >= t ? Math.max(btn.value, btn.pressed ? 1 : 0) : 0;
|
|
1081
|
+
});
|
|
1082
|
+
if (v > 0)
|
|
1083
|
+
out.pad = true;
|
|
1084
|
+
}
|
|
1085
|
+
else if ('pointer' in b) {
|
|
1086
|
+
const r = this.linkedPointers;
|
|
1087
|
+
// A press shorter than a tick still holds for one tick (the sources keep it).
|
|
1088
|
+
if (r?.held(b.pointer)) {
|
|
1089
|
+
v = 1;
|
|
1090
|
+
if (r.fired(b.pointer))
|
|
1091
|
+
out.fresh = true;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
else if ('xr' in b) {
|
|
1095
|
+
const r = this.linkedPointers;
|
|
1096
|
+
if (r?.xrHeld(b.xr, b.hand)) {
|
|
1097
|
+
v = 1;
|
|
1098
|
+
if (r.xrFired(b.xr, b.hand))
|
|
1099
|
+
out.fresh = true;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
else if ('gesture' in b) {
|
|
1103
|
+
const g = this.linkedGestures;
|
|
1104
|
+
if (g?.active(b.gesture)) {
|
|
1105
|
+
v = 1;
|
|
1106
|
+
// A tap on consecutive ticks is a new press each time.
|
|
1107
|
+
if (g.fired(b.gesture))
|
|
1108
|
+
out.fresh = true;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
else if ('chord' in b) {
|
|
1112
|
+
// Every member down, in any order. Members' own triggers are ignored.
|
|
1113
|
+
v = b.chord.length > 0 ? 1 : 0;
|
|
1114
|
+
const fresh = out.fresh;
|
|
1115
|
+
for (const m of b.chord) {
|
|
1116
|
+
if (this.rawValue(m) <= 0) {
|
|
1117
|
+
v = 0;
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
out.fresh = fresh;
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
v = this.padValue(b.pad, (p) => {
|
|
1125
|
+
const raw = (p.axes[b.padAxis] ?? 0) * b.dir;
|
|
1126
|
+
const t = b.threshold ?? DEFAULT_AXIS_THRESHOLD;
|
|
1127
|
+
return raw >= t ? Math.min(1, raw) : 0;
|
|
1128
|
+
});
|
|
1129
|
+
if (v > 0)
|
|
1130
|
+
out.pad = true;
|
|
1131
|
+
}
|
|
1132
|
+
return v;
|
|
1133
|
+
}
|
|
1134
|
+
padValue(index, read) {
|
|
1135
|
+
if (index !== undefined) {
|
|
1136
|
+
const p = this.pads[index];
|
|
1137
|
+
return p && p.connected !== false ? read(p) : 0;
|
|
1138
|
+
}
|
|
1139
|
+
let best = 0;
|
|
1140
|
+
for (const p of this.pads) {
|
|
1141
|
+
if (!p || p.connected === false)
|
|
1142
|
+
continue;
|
|
1143
|
+
const v = read(p);
|
|
1144
|
+
if (v > best)
|
|
1145
|
+
best = v;
|
|
1146
|
+
}
|
|
1147
|
+
return best;
|
|
1148
|
+
}
|
|
1149
|
+
// ---------------------------------------------------------------- low-level feed
|
|
1150
|
+
// Public so tests, replays and automated playtests can drive input without a DOM.
|
|
1151
|
+
keyDown(code) {
|
|
1152
|
+
const hooks = this.feedHooks;
|
|
1153
|
+
for (let i = 0; i < hooks.length; i++)
|
|
1154
|
+
hooks[i].keyDown(code);
|
|
1155
|
+
this.lastDevice = 'keyboard';
|
|
1156
|
+
if (this.listenState) {
|
|
1157
|
+
this.settleListen(code === 'Escape' ? null : { key: code });
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
if (!this.keysDown.has(code)) {
|
|
1161
|
+
if (this.queueTaps && (this.keysTapped.has(code) || this.keyRepeats.has(code))) {
|
|
1162
|
+
queueRepeat(this.keyRepeats, code);
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
this.keysTapped.add(code);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
this.keysDown.add(code);
|
|
1169
|
+
}
|
|
1170
|
+
keyUp(code) {
|
|
1171
|
+
const hooks = this.feedHooks;
|
|
1172
|
+
for (let i = 0; i < hooks.length; i++)
|
|
1173
|
+
hooks[i].keyUp(code);
|
|
1174
|
+
this.keysDown.delete(code);
|
|
1175
|
+
}
|
|
1176
|
+
buttonDown(button) {
|
|
1177
|
+
const hooks = this.feedHooks;
|
|
1178
|
+
for (let i = 0; i < hooks.length; i++)
|
|
1179
|
+
hooks[i].buttonDown(button);
|
|
1180
|
+
if (this.listenState) {
|
|
1181
|
+
this.settleListen({ mouse: button });
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (button === 0 && this.pointer.type !== 'touch') {
|
|
1185
|
+
this.linkedLook?.orbitDown(this.clientX, this.clientY);
|
|
1186
|
+
}
|
|
1187
|
+
if (!this.buttonsDown.has(button)) {
|
|
1188
|
+
if (this.queueTaps && (this.buttonsTapped.has(button) || this.buttonRepeats.has(button))) {
|
|
1189
|
+
queueRepeat(this.buttonRepeats, button);
|
|
1190
|
+
}
|
|
1191
|
+
else {
|
|
1192
|
+
this.buttonsTapped.add(button);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
this.buttonsDown.add(button);
|
|
1196
|
+
if (button === 0)
|
|
1197
|
+
this.pointer.down = true;
|
|
1198
|
+
}
|
|
1199
|
+
buttonUp(button) {
|
|
1200
|
+
const hooks = this.feedHooks;
|
|
1201
|
+
for (let i = 0; i < hooks.length; i++)
|
|
1202
|
+
hooks[i].buttonUp(button);
|
|
1203
|
+
this.buttonsDown.delete(button);
|
|
1204
|
+
if (button === 0) {
|
|
1205
|
+
this.pointer.down = false;
|
|
1206
|
+
this.linkedLook?.orbitUp();
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
wheel(dx, dy) {
|
|
1210
|
+
const hooks = this.feedHooks;
|
|
1211
|
+
for (let i = 0; i < hooks.length; i++)
|
|
1212
|
+
hooks[i].wheel(dx, dy);
|
|
1213
|
+
this.wheelAccX += dx;
|
|
1214
|
+
this.wheelAccY += dy;
|
|
1215
|
+
}
|
|
1216
|
+
/** Set the pointer position from client coordinates (runs the transform). */
|
|
1217
|
+
pointerAt(clientX, clientY, type = 'mouse') {
|
|
1218
|
+
const hooks = this.feedHooks;
|
|
1219
|
+
for (let i = 0; i < hooks.length; i++)
|
|
1220
|
+
hooks[i].pointerAt(clientX, clientY, type);
|
|
1221
|
+
const p = this.transform ? this.transform(clientX, clientY) : { x: clientX, y: clientY };
|
|
1222
|
+
this.pointer.type = type;
|
|
1223
|
+
this.lastDevice = type;
|
|
1224
|
+
this.clientX = clientX;
|
|
1225
|
+
this.clientY = clientY;
|
|
1226
|
+
if (type !== 'touch')
|
|
1227
|
+
this.linkedLook?.orbitMove(clientX, clientY);
|
|
1228
|
+
if (p) {
|
|
1229
|
+
this.pointer.x = p.x;
|
|
1230
|
+
this.pointer.y = p.y;
|
|
1231
|
+
this.pointer.inside = true;
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
this.pointer.inside = false;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
/** Release every key and button (focus loss, tab hidden). */
|
|
1238
|
+
releaseAll() {
|
|
1239
|
+
const hooks = this.feedHooks;
|
|
1240
|
+
for (let i = 0; i < hooks.length; i++)
|
|
1241
|
+
hooks[i].releaseAll();
|
|
1242
|
+
this.keysDown.clear();
|
|
1243
|
+
this.buttonsDown.clear();
|
|
1244
|
+
this.keyRepeats.clear();
|
|
1245
|
+
this.buttonRepeats.clear();
|
|
1246
|
+
this.pointer.down = false;
|
|
1247
|
+
this.linkedLook?.orbitUp();
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* An overlay control fired (ws/input-12): a button the player clicked, tapped or activated
|
|
1251
|
+
* with Enter or the pad, named by its stable id (`controlId` in ui). Feed hooks see it, so a
|
|
1252
|
+
* recording carries it in its `ui` lane on the tick it happened; nothing else changes. The
|
|
1253
|
+
* control's own handler has already run. `createGame` calls it for every overlay control.
|
|
1254
|
+
*/
|
|
1255
|
+
control(id) {
|
|
1256
|
+
const hooks = this.feedHooks;
|
|
1257
|
+
for (let i = 0; i < hooks.length; i++)
|
|
1258
|
+
hooks[i].control?.(id);
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Replay an overlay control's activation (ws/input-12): emits `'control'` with `id`, whose
|
|
1262
|
+
* listener (`createGame`: `Overlay.activate`) fires the control. `createInputPlayback` calls it
|
|
1263
|
+
* for the recording's `ui` lane, before that tick's `update()`, so the handler runs on the
|
|
1264
|
+
* tick it ran live. Feed hooks do not see it.
|
|
1265
|
+
*/
|
|
1266
|
+
replayControl(id) {
|
|
1267
|
+
this.events.emit('control', id);
|
|
1268
|
+
}
|
|
1269
|
+
// ---------------------------------------------------------------- DOM
|
|
1270
|
+
/**
|
|
1271
|
+
* Listen to the DOM. Keyboard: only bound keys are `preventDefault`ed, and never when a
|
|
1272
|
+
* modifier (Ctrl/Meta/Alt) is held, the target is a text field/button/link, input is
|
|
1273
|
+
* blocked, or the document lacks focus. That keeps browser shortcuts working and avoids
|
|
1274
|
+
* scrolling hijacks inside the itch.io iframe.
|
|
1275
|
+
*/
|
|
1276
|
+
attach(opts) {
|
|
1277
|
+
this.detach();
|
|
1278
|
+
const target = opts.pointerTarget;
|
|
1279
|
+
const win = target.ownerDocument.defaultView ?? window;
|
|
1280
|
+
const doc = target.ownerDocument;
|
|
1281
|
+
const keyTarget = opts.keyTarget ?? win;
|
|
1282
|
+
const prevTouchAction = target.style.touchAction;
|
|
1283
|
+
if (opts.disableTouchScroll !== false)
|
|
1284
|
+
target.style.touchAction = 'none';
|
|
1285
|
+
const onKeyDown = (ev) => {
|
|
1286
|
+
const e = ev;
|
|
1287
|
+
if (isInteractiveTarget(e.target)) {
|
|
1288
|
+
// Global keys still count on buttons/links (never in text entry); the control keeps
|
|
1289
|
+
// the default action.
|
|
1290
|
+
const passes = (this.globalKeys.has(e.code) && !isTextEntryTarget(e.target)) ||
|
|
1291
|
+
(this.buttonKeys.has(e.code) && isButtonTarget(e.target));
|
|
1292
|
+
if (!e.repeat && passes)
|
|
1293
|
+
this.keyDown(e.code);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (!e.repeat)
|
|
1297
|
+
this.keyDown(e.code);
|
|
1298
|
+
if (this.boundKeys.has(e.code) &&
|
|
1299
|
+
!e.ctrlKey &&
|
|
1300
|
+
!e.metaKey &&
|
|
1301
|
+
!e.altKey &&
|
|
1302
|
+
!this.blocked() &&
|
|
1303
|
+
doc.hasFocus()) {
|
|
1304
|
+
e.preventDefault();
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
const onKeyUp = (ev) => {
|
|
1308
|
+
this.keyUp(ev.code);
|
|
1309
|
+
};
|
|
1310
|
+
const dispatcher = createPointerDispatcher(target, {
|
|
1311
|
+
transform: this.transform,
|
|
1312
|
+
hoverWhileClaimed: opts.hoverWhileClaimed,
|
|
1313
|
+
bounds: opts.bounds,
|
|
1314
|
+
});
|
|
1315
|
+
this.dispatcherValue = dispatcher;
|
|
1316
|
+
// The feed sees every pointer, claimed or not, so recordings stay complete and identical.
|
|
1317
|
+
// Hooks see every record (the recording's `ptr` lane); while `replayPointer` dispatches, the
|
|
1318
|
+
// raw state is left alone (the playback feeds it from the `p` and `b` lanes).
|
|
1319
|
+
const hooks = this.feedHooks;
|
|
1320
|
+
const notePointer = (phase, p) => {
|
|
1321
|
+
for (let i = 0; i < hooks.length; i++)
|
|
1322
|
+
hooks[i].pointer?.(phase, p);
|
|
1323
|
+
return this.injecting;
|
|
1324
|
+
};
|
|
1325
|
+
const consumer = {
|
|
1326
|
+
pointerDown: (p) => {
|
|
1327
|
+
if (notePointer('down', p))
|
|
1328
|
+
return;
|
|
1329
|
+
if (!p.primary && p.device === 'touch')
|
|
1330
|
+
return;
|
|
1331
|
+
this.pointerAt(p.x, p.y, p.device);
|
|
1332
|
+
this.buttonDown(p.button);
|
|
1333
|
+
// Clicking the canvas should give the iframe keyboard focus.
|
|
1334
|
+
if (!doc.hasFocus())
|
|
1335
|
+
win.focus();
|
|
1336
|
+
},
|
|
1337
|
+
pointerMove: (p) => {
|
|
1338
|
+
if (notePointer('move', p))
|
|
1339
|
+
return;
|
|
1340
|
+
if (!p.primary)
|
|
1341
|
+
return;
|
|
1342
|
+
this.pointerAt(p.x, p.y, p.device);
|
|
1343
|
+
},
|
|
1344
|
+
pointerUp: (p) => {
|
|
1345
|
+
if (notePointer('up', p))
|
|
1346
|
+
return;
|
|
1347
|
+
if (!p.primary && p.device === 'touch')
|
|
1348
|
+
return;
|
|
1349
|
+
this.buttonUp(p.button);
|
|
1350
|
+
},
|
|
1351
|
+
pointerCancel: (p) => {
|
|
1352
|
+
if (notePointer('cancel', p))
|
|
1353
|
+
return;
|
|
1354
|
+
if (p.primary)
|
|
1355
|
+
this.buttonUp(0);
|
|
1356
|
+
},
|
|
1357
|
+
pointerLeave: (p) => {
|
|
1358
|
+
if (notePointer('leave', p))
|
|
1359
|
+
return;
|
|
1360
|
+
if (p.device === 'mouse')
|
|
1361
|
+
this.pointer.inside = false;
|
|
1362
|
+
},
|
|
1363
|
+
cancelAll: () => {
|
|
1364
|
+
for (let i = 0; i < hooks.length; i++)
|
|
1365
|
+
hooks[i].cancelPointers?.();
|
|
1366
|
+
if (!this.injecting)
|
|
1367
|
+
this.releaseAll();
|
|
1368
|
+
},
|
|
1369
|
+
};
|
|
1370
|
+
const removeConsumer = dispatcher.add(consumer, POINTER_PRIORITY.feed, true);
|
|
1371
|
+
const onWheel = (e) => {
|
|
1372
|
+
// deltaMode 1 = lines, 2 = pages; normalise roughly to pixels.
|
|
1373
|
+
const k = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 400 : 1;
|
|
1374
|
+
this.wheel(e.deltaX * k, e.deltaY * k);
|
|
1375
|
+
if (this.boundWheel && !this.blocked())
|
|
1376
|
+
e.preventDefault();
|
|
1377
|
+
};
|
|
1378
|
+
const onContextMenu = (e) => {
|
|
1379
|
+
if (this.boundMouse.has(2) && !this.blocked())
|
|
1380
|
+
e.preventDefault();
|
|
1381
|
+
};
|
|
1382
|
+
keyTarget.addEventListener('keydown', onKeyDown);
|
|
1383
|
+
keyTarget.addEventListener('keyup', onKeyUp);
|
|
1384
|
+
target.addEventListener('wheel', onWheel, { passive: false });
|
|
1385
|
+
target.addEventListener('contextmenu', onContextMenu);
|
|
1386
|
+
this.attached = opts;
|
|
1387
|
+
this.linkedGestures?.attach({
|
|
1388
|
+
pointerTarget: target,
|
|
1389
|
+
disableTouchScroll: opts.disableTouchScroll,
|
|
1390
|
+
dispatcher,
|
|
1391
|
+
});
|
|
1392
|
+
this.linkedPointers?.attach(target, dispatcher);
|
|
1393
|
+
this.linkedLook?.attach(target, dispatcher);
|
|
1394
|
+
this.events.emit('attach', dispatcher);
|
|
1395
|
+
this.detachFn = () => {
|
|
1396
|
+
this.events.emit('detach', dispatcher);
|
|
1397
|
+
this.linkedGestures?.detach();
|
|
1398
|
+
this.linkedPointers?.detach();
|
|
1399
|
+
this.linkedLook?.detach();
|
|
1400
|
+
this.attached = null;
|
|
1401
|
+
keyTarget.removeEventListener('keydown', onKeyDown);
|
|
1402
|
+
keyTarget.removeEventListener('keyup', onKeyUp);
|
|
1403
|
+
target.removeEventListener('wheel', onWheel);
|
|
1404
|
+
target.removeEventListener('contextmenu', onContextMenu);
|
|
1405
|
+
removeConsumer();
|
|
1406
|
+
dispatcher.destroy();
|
|
1407
|
+
this.dispatcherValue = null;
|
|
1408
|
+
target.style.touchAction = prevTouchAction;
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Dispatch a recorded pointer event on the attached pointer target (ws/input-5), so every
|
|
1413
|
+
* dispatcher consumer (gestures, look, touch zones, the router) sees it as it saw the original;
|
|
1414
|
+
* the feed's own key/button/pointer state is left to the playback's `b` and `p` lanes.
|
|
1415
|
+
* `createInputPlayback` calls it for the recording's `ptr` lane. `age` (ws/input-7) is how
|
|
1416
|
+
* many ms ago the event happened on the consumers' clocks (`PointerRecord.age`; negative is
|
|
1417
|
+
* ahead); omitted, it happened now. Returns false (and does nothing) while detached or
|
|
1418
|
+
* without `PointerEvent`.
|
|
1419
|
+
*/
|
|
1420
|
+
replayPointer(phase, id, device, clientX, clientY, button, primary, age = 0) {
|
|
1421
|
+
const d = this.dispatcherValue;
|
|
1422
|
+
if (!d || typeof PointerEvent !== 'function')
|
|
1423
|
+
return false;
|
|
1424
|
+
const type = phase === 'down'
|
|
1425
|
+
? 'pointerdown'
|
|
1426
|
+
: phase === 'move'
|
|
1427
|
+
? 'pointermove'
|
|
1428
|
+
: phase === 'up'
|
|
1429
|
+
? 'pointerup'
|
|
1430
|
+
: phase === 'cancel'
|
|
1431
|
+
? 'pointercancel'
|
|
1432
|
+
: 'pointerleave';
|
|
1433
|
+
const e = new PointerEvent(type, {
|
|
1434
|
+
pointerId: id,
|
|
1435
|
+
pointerType: device,
|
|
1436
|
+
isPrimary: primary,
|
|
1437
|
+
button,
|
|
1438
|
+
clientX,
|
|
1439
|
+
clientY,
|
|
1440
|
+
});
|
|
1441
|
+
this.injecting = true;
|
|
1442
|
+
d.replayAge = age;
|
|
1443
|
+
try {
|
|
1444
|
+
d.target.dispatchEvent(e);
|
|
1445
|
+
}
|
|
1446
|
+
finally {
|
|
1447
|
+
this.injecting = false;
|
|
1448
|
+
d.replayAge = null;
|
|
1449
|
+
}
|
|
1450
|
+
return true;
|
|
1451
|
+
}
|
|
1452
|
+
detach() {
|
|
1453
|
+
this.detachFn?.();
|
|
1454
|
+
this.detachFn = null;
|
|
1455
|
+
this.releaseAll();
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
/** Same length and the same binding objects in order. */
|
|
1459
|
+
function sameList(a, b) {
|
|
1460
|
+
if (a === b)
|
|
1461
|
+
return true;
|
|
1462
|
+
if (!a || a.length !== b.length)
|
|
1463
|
+
return false;
|
|
1464
|
+
for (let i = 0; i < a.length; i++)
|
|
1465
|
+
if (a[i] !== b[i])
|
|
1466
|
+
return false;
|
|
1467
|
+
return true;
|
|
1468
|
+
}
|
|
1469
|
+
function clamp01(v) {
|
|
1470
|
+
return Math.min(1, Math.max(0, v));
|
|
1471
|
+
}
|
|
1472
|
+
function queueRepeat(repeats, k) {
|
|
1473
|
+
repeats.set(k, Math.min(MAX_QUEUED_TAPS, (repeats.get(k) ?? 0) + 1));
|
|
1474
|
+
}
|
|
1475
|
+
/** One queued repeat per key is used up each tick, except on the tick of its first tap. */
|
|
1476
|
+
function rememberActive(prev, down, tapped) {
|
|
1477
|
+
prev.clear();
|
|
1478
|
+
for (const k of down)
|
|
1479
|
+
prev.add(k);
|
|
1480
|
+
for (const k of tapped)
|
|
1481
|
+
prev.add(k);
|
|
1482
|
+
}
|
|
1483
|
+
function consumeRepeats(repeats, tappedNow) {
|
|
1484
|
+
for (const [k, n] of repeats) {
|
|
1485
|
+
if (tappedNow.has(k))
|
|
1486
|
+
continue;
|
|
1487
|
+
if (n <= 1)
|
|
1488
|
+
repeats.delete(k);
|
|
1489
|
+
else
|
|
1490
|
+
repeats.set(k, n - 1);
|
|
1491
|
+
}
|
|
1492
|
+
}
|