@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mandalorian007 / ThunderClap Gaming
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,334 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ThunderClap Engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A WebGPU game engine for the browser, written in TypeScript. It renders 2D and 3D with raw WebGPU, runs on desktop, tablet, phone and VR headsets, and has no runtime dependencies.
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="docs/media/last-rune.gif" alt="The Last Rune running on ThunderClap: title screen, the machine hall, then gems placed in a stone altar carry a lightning bolt to the gauge at exactly full charge." width="640" />
|
|
7
|
+
<br />
|
|
8
|
+
<sub><a href="https://mandalorian007.github.io/last-rune/">The Last Rune</a>, the first game built on ThunderClap</sub>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
## Contents
|
|
12
|
+
|
|
13
|
+
- [What it is](#what-it-is)
|
|
14
|
+
- [Quick start](#quick-start)
|
|
15
|
+
- [Your first game](#your-first-game)
|
|
16
|
+
- [Guides](#guides)
|
|
17
|
+
- [Modules](#modules)
|
|
18
|
+
- [Testing](#testing)
|
|
19
|
+
- [Status](#status)
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
`@thunderclap-engine/engine` knows nothing about any game. A game supplies its scenes, rules and art; the engine supplies the canvas, the frame loop, rendering, post-processing, input, UI, audio, saves and the page lifecycle. [docs/VISION.md](docs/VISION.md) says what ThunderClap is for and where it is going.
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
Each line names where to start and links its guide or reference. [docs/OFFERING.md](docs/OFFERING.md) has the longer list.
|
|
28
|
+
|
|
29
|
+
**Start and run**
|
|
30
|
+
|
|
31
|
+
- **One boot call.** Canvas, renderer, post, particles, input, overlay, audio, save, host and a fixed 60 Hz loop in one ctx: `createGame(opts)` ([Getting started](docs/guide/getting-started.md#the-shape-of-a-game)).
|
|
32
|
+
- **Client mode** for a server-authoritative game: no save, the loop runs once per rendered frame: `createGame({ sim: false })`, `ctx.hooks.frame` ([Getting started](docs/guide/getting-started.md)).
|
|
33
|
+
- **Every page it runs in.** A top-level tab, a cross-origin iframe (itch.io), Electron, Capacitor and an installable PWA: `Host`, `createWebHost`, `@thunderclap-engine/host-electron`, `@thunderclap-engine/host-capacitor`, `@thunderclap-engine/host-pwa` ([Ship it](docs/guide/getting-started.md#ship-it), [host](src/host/README.md)).
|
|
34
|
+
- **Page lifecycle.** Hiding stops the loop and flushes the save; device loss shows a reload screen: `ctx.hooks.hide`, `ctx.hooks.deviceLost` ([platform](src/platform/README.md)).
|
|
35
|
+
|
|
36
|
+
**Render**
|
|
37
|
+
|
|
38
|
+
- **2D and 3D on one renderer.** HDR colour and emissive targets, instanced sprites and meshes, one instance format: `frame.sprite`, `frame.mesh`, `Transform` ([2D guide](docs/guide/2d-game.md), [3D guide](docs/guide/3d-game.md)).
|
|
39
|
+
- **Cameras.** `Camera.ortho2d` by default, `Camera.perspective` with depth, split views and stereo: `renderer.render(frame, camera, target)` ([render](src/render/README.md)).
|
|
40
|
+
- **Materials in your own WGSL.** A prelude gives you the lights, shadow and fog: `renderer.createMaterial({ label, wgsl })`, `materialPrelude` ([2D guide](docs/guide/2d-game.md#write-your-own-shader)).
|
|
41
|
+
- **Lights, shadows and fog.** Ambient, a sun with a shadow map, 16 point and 8 spot lights, layer masks: `frame.lights` ([3D guide](docs/guide/3d-game.md#add-lights)).
|
|
42
|
+
- **PBR.** The glTF metallic-roughness material, with normal maps and skinning: `createStandardMaterial` ([3D guide](docs/guide/3d-game.md#use-pbr-materials)).
|
|
43
|
+
- **Skinning.** GPU skinning in the scene and shadow passes: `VERTEX_LAYOUT_SKINNED`, `SkinPalette`, `skinPose` ([Characters](docs/guide/characters.md)).
|
|
44
|
+
- **LOD and culling.** Per-mesh bounds, frustum culling and level selection by screen size: `mesh.bounds`, `MeshLod`, `selectLod`, `InstanceList` ([3D guide](docs/guide/3d-game.md#keep-it-fast-with-lod)).
|
|
45
|
+
- **2D kit.** Tile layers, flipbooks, parallax and a follow camera: `createTileLayer`, `Flipbook`, `createParallax`, `createFollow2D` ([2D guide](docs/guide/2d-game.md#build-a-tile-map)).
|
|
46
|
+
- **Textures.** Images, texture arrays with GPU mips, KTX2: `loadTexture`, `loadTextureArray`, `uploadKtx2` ([render](src/render/README.md)).
|
|
47
|
+
|
|
48
|
+
**Post and FX**
|
|
49
|
+
|
|
50
|
+
- **Post stack.** Bloom, tone map, grade, vignette, god rays, mist, AO, depth of field, TAA, SSR, motion blur, auto exposure and finishing passes, with quality tiers: `ctx.post` (`PostStack`), `postQualityFor` ([Style and post](docs/guide/style-and-post.md)).
|
|
51
|
+
- **GPU particles.** 2D or world space, soft, lit, with sprite sheets, forces, collision and effect files: `ParticleSystem`, `createEffect` ([2D guide](docs/guide/2d-game.md#particles), [fx](src/fx/README.md)).
|
|
52
|
+
- **World FX.** Sky, water, trails, decals, billboards and debug lines: `Sky`, `Water`, `Trail`, `Decals`, `createBillboards`, `createDebugDraw` ([Style and post](docs/guide/style-and-post.md#sky-and-water)).
|
|
53
|
+
|
|
54
|
+
**Content**
|
|
55
|
+
|
|
56
|
+
- **glTF 2.0.** Meshes, materials, skins, animations, morph targets and common extensions: `parseGltf`, `loadGltf` ([3D guide](docs/guide/3d-game.md#load-a-gltf-model), [assets](src/assets/README.md)).
|
|
57
|
+
- **Typed asset loading.** A manifest, a progress bar and a cache: `defineManifest`, `createBundle`, `createAssetCache`, `packAtlas` ([assets](src/assets/README.md)).
|
|
58
|
+
- **Procedural geometry.** Boxes, spheres, capsules, heightfields and more: `box`, `sphere`, `createHeightfield` in `@thunderclap-engine/engine/geom` ([geom](src/geom/README.md)).
|
|
59
|
+
- **Collision.** A static collider and capsule or circle sliding: `createStaticCollider`, `slide`, `slide2` ([geom](src/geom/README.md)).
|
|
60
|
+
- **Voxel worlds.** Greedy-meshed chunks with baked AO from a Worker, culled per chunk, with a block-picking ray: `createVoxelGrid`, `createVoxelChunks`, `raycastVoxels` ([3D guide](docs/guide/3d-game.md#voxel-worlds), [voxel](src/voxel/README.md)).
|
|
61
|
+
- **Navigation.** Grids, navmeshes, A*, flow fields and crowds: `createNavGrid`, `findPath`, `bakeNavMesh`, `createCrowd` ([nav](src/nav/README.md)).
|
|
62
|
+
- **Animation.** Clips, a mixer, an animation graph, IK, retargeting and springs: `AnimationMixer`, `createAnimGraph`, `twoBoneIk`, `retargetPose` ([Characters](docs/guide/characters.md), [anim](src/anim/README.md)).
|
|
63
|
+
- **Sockets.** A transform that follows a joint, for hats and swords: `attachTransform` ([Characters](docs/guide/characters.md#attach-a-sword-to-the-hand)).
|
|
64
|
+
- **Tweens.** Easing, sequences and springs: `createTweens` ([tween](src/tween/README.md)).
|
|
65
|
+
|
|
66
|
+
**Input**
|
|
67
|
+
|
|
68
|
+
- **One action map** over keyboard, mouse, touch, pen and gamepad: `createGame({ input: { bindings } })`, `ctx.input.pressed` ([Input](docs/guide/input.md)).
|
|
69
|
+
- **Gestures.** Tap, double tap, long press, pan, drag and pinch, and a loupe for small targets: `ctx.gestures`, `input.magnify` ([Input](docs/guide/input.md#gestures)).
|
|
70
|
+
- **Picking.** Every pointer is a ray; test it against planes, boxes or meshes: `ctx.pointers`, `meshTester` ([Input](docs/guide/input.md#pick-things-with-pointer-rays)).
|
|
71
|
+
- **First and third person.** Pointer lock, touch zones, sticks and a follow camera: `createLook`, `Follow`, `createTouchZones` ([Input](docs/guide/input.md#first-person-look), [Characters](docs/guide/characters.md#follow-the-character-with-the-camera)).
|
|
72
|
+
- **Record and replay.** Record a session and run it back tick for tick: `createGame({ input: { record, replay } })`, `ctx.replay(rec)` mid-session, `createInputPlayback` ([Input](docs/guide/input.md#record-and-replay-a-session)).
|
|
73
|
+
- **WebXR.** Enter VR, render each eye through the post stack, controllers and hands as rays: `createGame({ xr })`, `ctx.xr.enter()` ([VR](docs/guide/xr.md)).
|
|
74
|
+
|
|
75
|
+
**UI, audio and plumbing**
|
|
76
|
+
|
|
77
|
+
- **DOM overlay in layout regions** for phone, tablet and desktop, with safe areas and text scale: `ctx.ui`, `ctx.ui.layout`, `LayoutSpec` ([UI and HUD](docs/guide/ui-and-hud.md)).
|
|
78
|
+
- **HUD widgets.** Meters, orbs, rings, a compass, a minimap, a reticle, slots and floating numbers: `ui.meter`, `createOrbMeter`, `ui.minimap`, `ui.floats` ([UI and HUD](docs/guide/ui-and-hud.md#hud-widgets)).
|
|
79
|
+
- **World UI.** DOM anchored to world points and panels in 3D or VR: `ui.anchors(() => camera)`, `createWorldPanel` ([UI and HUD](docs/guide/ui-and-hud.md#pin-dom-to-the-world)).
|
|
80
|
+
- **Audio.** Buses, a synth, sound recipes, music stems, spatial voices and zones: `ctx.audio`, `createMusic` ([audio](src/audio/README.md)).
|
|
81
|
+
- **Saves.** Versioned, schema-checked slots with migrations: `createSaveStore`, `s`, `createSlotManager` ([Testing your game](docs/guide/testing-your-game.md#test-the-save-schema), [save](src/save/README.md)).
|
|
82
|
+
- **Analytics.** Typed, batched events to a sink you supply: `createAnalytics` ([analytics](src/analytics/README.md)).
|
|
83
|
+
- **i18n.** String tables, plurals through `Intl`, a runtime locale swap: `createI18n` ([i18n](src/i18n/README.md)).
|
|
84
|
+
- **Test kit.** A fake host, a fixed clock and a fake GPU for a game's unit tests: `@thunderclap-engine/engine/testing` ([Testing your game](docs/guide/testing-your-game.md), [testing](src/testing/README.md)).
|
|
85
|
+
|
|
86
|
+
### Requirements
|
|
87
|
+
|
|
88
|
+
- A WebGPU browser: Chrome or Edge 113+, or Safari 26+ (including iOS and iPadOS).
|
|
89
|
+
- To develop: Node 22 or newer and pnpm (the version is pinned in `package.json`).
|
|
90
|
+
|
|
91
|
+
### Design rules
|
|
92
|
+
|
|
93
|
+
1. **No game nouns.** Code joins the engine only if a shipped game uses it, the next game plausibly would, and its API names nothing from that game.
|
|
94
|
+
2. **One direction.** Games import the engine; the engine never imports a game.
|
|
95
|
+
3. **No runtime dependencies.** WebGPU is the only renderer.
|
|
96
|
+
4. **Browser-only storage.** Saves go through `Host.storage`, which is `localStorage` on the web. The engine makes no network calls.
|
|
97
|
+
5. **Seams, not speculation.** Each capability arrives with the game that needs it (VISION rule 6). 3D and XR came through seams the 2D engine already had (`Surface`, `Camera`, the frame loop, `Host`, `PointerSource`): `XrSurface`, an XR host and `createGame({ xr })` plug into them, and the 2D path stays pixel-identical.
|
|
98
|
+
|
|
99
|
+
## Quick start
|
|
100
|
+
|
|
101
|
+
### Install
|
|
102
|
+
|
|
103
|
+
The engine is on npm. Add it to any Vite (or other ESM bundler) project:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
pnpm add @thunderclap-engine/engine
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
It ships ESM JavaScript and `.d.ts` files with no runtime dependencies. The hosts are separate packages: `pnpm add @thunderclap-engine/host-electron`, `@thunderclap-engine/host-capacitor` or `@thunderclap-engine/host-pwa`. The type declarations use the browser's WebGPU types (`GPUDevice`, `GPUTexture`). TypeScript 7's DOM lib has them. If your TypeScript reports `Cannot find name 'GPUDevice'`, run `pnpm add -D @webgpu/types` and add `"@webgpu/types"` to `compilerOptions.types`.
|
|
110
|
+
|
|
111
|
+
### Start from a template
|
|
112
|
+
|
|
113
|
+
`pnpm create-game` copies a template into a folder of its own, with the engine linked to your clone of this repo:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
git clone https://github.com/Mandalorian007/thunderclap-engine.git
|
|
117
|
+
cd thunderclap-engine
|
|
118
|
+
pnpm install
|
|
119
|
+
pnpm create-game ../my-game # the puzzle template; --list shows all four
|
|
120
|
+
cd ../my-game && pnpm install && pnpm dev
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Open `http://127.0.0.1:5173`. This is the puzzle template running:
|
|
124
|
+
|
|
125
|
+
<p align="center">
|
|
126
|
+
<img src="docs/media/quick-start-puzzle.png" alt="The puzzle template in a desktop browser: a 4×4 board of face-down tiles under a glowing cursor, a moves and pairs counter at the top and a New game button." width="640" />
|
|
127
|
+
</p>
|
|
128
|
+
|
|
129
|
+
Pick another template with `--template <name>`:
|
|
130
|
+
|
|
131
|
+
| Template | Start here if you want |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `puzzle` | a 2D board game: one scene, pure rules with tests, a versioned save, a HUD laid out on every screen |
|
|
134
|
+
| `platformer` | a 2D action game: tile maps, a flipbook player, parallax, a follow camera, touch zones, a pause menu, sounds |
|
|
135
|
+
| `first-person` | a 3D world you walk through: `Look`, Worker-built chunks, a sun with shadows, fog, weather, world labels |
|
|
136
|
+
| `third-person` | a 3D character game: a skinned, animated hero, a follow camera, sockets, foot IK, PBR, enemies with health bars |
|
|
137
|
+
|
|
138
|
+
The new folder is a standalone project with `@thunderclap-engine/engine` as a dependency. Each template ships a `CLAUDE.md` that names the engine concepts it uses. `--engine <spec>` points it at another copy of the engine (`"github:Mandalorian007/thunderclap-engine"`, `link:../engine`), `--dry-run` prints the plan, and a non-empty folder needs `--force`. [Getting started](docs/guide/getting-started.md) walks through the rest.
|
|
139
|
+
|
|
140
|
+
The npm package ships compiled ESM (`pnpm build` makes it; [Releasing](docs/releasing.md) covers how a version is published). Inside this repo the workspace links to the TypeScript source instead, which Vite compiles. Import only from the public entry points listed under [Modules](#modules). Anything not re-exported from a module's `index.ts` is internal.
|
|
141
|
+
|
|
142
|
+
## Your first game
|
|
143
|
+
|
|
144
|
+
`pnpm example` (in the engine clone) serves `examples/hello` at `http://127.0.0.1:5173`: a glowing square; click or press Space to flare it. This is the whole game (`examples/hello/main.ts`):
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { createGame } from '@thunderclap-engine/engine';
|
|
148
|
+
import type { Scene } from '@thunderclap-engine/engine/core';
|
|
149
|
+
import { type DrawParams, type Frame, hex } from '@thunderclap-engine/engine/render';
|
|
150
|
+
import { createSaveStore, s } from '@thunderclap-engine/engine/save';
|
|
151
|
+
|
|
152
|
+
const ctx = await createGame({
|
|
153
|
+
root: document.getElementById('app')!,
|
|
154
|
+
title: 'Hello ThunderClap',
|
|
155
|
+
save: (host) =>
|
|
156
|
+
createSaveStore({
|
|
157
|
+
game: 'hello',
|
|
158
|
+
slot: 'main',
|
|
159
|
+
version: 1,
|
|
160
|
+
defaults: () => ({ flares: 0 }),
|
|
161
|
+
schema: s.object({ flares: s.number() }),
|
|
162
|
+
host,
|
|
163
|
+
}),
|
|
164
|
+
input: { bindings: { flare: [{ mouse: 0 }, { key: 'Space' }] } },
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (ctx) {
|
|
168
|
+
// null means no WebGPU: the unsupported screen is already showing.
|
|
169
|
+
const square: DrawParams = { x: 960, y: 540, w: 240, h: 240, rotation: 0, emissive: 1 };
|
|
170
|
+
let glow = 1;
|
|
171
|
+
|
|
172
|
+
const hello: Scene<typeof ctx, Frame> = {
|
|
173
|
+
update(dt) {
|
|
174
|
+
square.rotation = (square.rotation ?? 0) + dt;
|
|
175
|
+
if (ctx.input.pressed('flare')) {
|
|
176
|
+
glow = 4;
|
|
177
|
+
ctx.save.update((d) => ({ flares: d.flares + 1 }));
|
|
178
|
+
}
|
|
179
|
+
glow += (1 - glow) * Math.min(1, dt * 3);
|
|
180
|
+
},
|
|
181
|
+
render(frame) {
|
|
182
|
+
frame.clear(0.02, 0.02, 0.04);
|
|
183
|
+
square.tint = hex('#3fe0ff', glow); // HDR tint above 1 blooms
|
|
184
|
+
frame.sprite(null, square);
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
ctx.stack.push(hello);
|
|
189
|
+
ctx.loop.start();
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The world is a 1920×1080 logical space by default (origin top-left, y down), scaled to fit any screen. `createGame` returns `null` when the browser has no WebGPU. A game usually widens `ctx` with its own nouns (`Object.assign(ctx, { ... })`) so every scene gets one typed ctx.
|
|
194
|
+
|
|
195
|
+
### How a frame runs
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
createGame()
|
|
199
|
+
└─ GameLoop (fixed 60 Hz update, variable-rate render; frames from the host: rAF, Electron, or an XRSession)
|
|
200
|
+
update(dt): input → pad nav → hooks.update → particles → scene stack
|
|
201
|
+
render: hooks.beforeRender → renderer.beginFrame → scene stack → endFrame → hooks.afterRender
|
|
202
|
+
|
|
203
|
+
renderer.endFrame()
|
|
204
|
+
1. upload globals and all instance data (one writeBuffer each)
|
|
205
|
+
2. pre-passes and drawable.prepare() (compute, uploads), on the first render() of the frame
|
|
206
|
+
3. per render(frame, camera, target): frustum cull, sort, a sun shadow pass when lights.sun.shadow is set,
|
|
207
|
+
then the scene pass → HDR colour + emissive targets (+ depth)
|
|
208
|
+
4. post stack (bloom, god rays, mist, vignette, grade) → swapchain (in VR: per eye, into the
|
|
209
|
+
session's layer, via renderer.resolve)
|
|
210
|
+
5. submit
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`renderer.render(frame, camera, target)` runs the recorded draw lists through a camera into a target, any number of times per frame; `endFrame` calls it with `renderer.camera` when nothing did. With the default `ortho2d` camera draw order is submission order (then `order`) and there is no depth attachment; a camera with depth sorts by view-space depth and tests depth. Consecutive draws with the same material and mesh batch into one instanced call.
|
|
214
|
+
|
|
215
|
+
## Guides
|
|
216
|
+
|
|
217
|
+
Short, task-shaped walkthroughs. Each opens with what you'll build and ends with where to go next. Every TypeScript block in them is typechecked against the engine by `pnpm check`, and fails it if it uses a deprecated name.
|
|
218
|
+
|
|
219
|
+
1. [Getting started](docs/guide/getting-started.md): scaffold, run, the shape of a game, and shipping to itch.io, Electron or a PWA.
|
|
220
|
+
2. [A 2D game](docs/guide/2d-game.md): sprites, the camera, your own WGSL, particles and layout regions.
|
|
221
|
+
3. [A 3D game](docs/guide/3d-game.md): perspective, meshes, glTF, lights, shadows, PBR, LOD and culling.
|
|
222
|
+
4. [Characters](docs/guide/characters.md): a skinned glTF, the animation mixer, sockets and the follow camera.
|
|
223
|
+
5. [Input](docs/guide/input.md): actions, gestures, pointer rays, `Look`, pads, touch zones, record and replay.
|
|
224
|
+
6. [UI and HUD](docs/guide/ui-and-hud.md): the overlay, regions, widgets, world anchors and panels, theme and text scale.
|
|
225
|
+
7. [Style and post](docs/guide/style-and-post.md): the post stack, quality tiers, FX, sky, water and weather.
|
|
226
|
+
8. [VR](docs/guide/xr.md): entering VR, controllers and hands, per-eye post, panels.
|
|
227
|
+
9. [Testing your game](docs/guide/testing-your-game.md): the `./testing` kit (fake host, fixed clock, fake GPU), replay tests, the harness pattern and `expect.poll`.
|
|
228
|
+
10. [Migrating to v1](docs/guide/migrating-to-v1.md): every 0.x name removed in 1.0.0, its v1 replacement and its module.
|
|
229
|
+
|
|
230
|
+
The module READMEs under `src/<module>/` are the full reference.
|
|
231
|
+
|
|
232
|
+
## Modules
|
|
233
|
+
|
|
234
|
+
| Import | What it holds |
|
|
235
|
+
| --- | --- |
|
|
236
|
+
| `@thunderclap-engine/engine` | `createGame(opts: CreateGameOptions)` (with `aux` for the renderer's aux targets), `EngineCtx`, `GameHooks` (with `HideReason`, `ShowReason`), `SimOption`, `FlushableSave`, `GameReplay` and `ReplayOptions` (`ctx.replay`, with `onEnd`), `XrOptions` and `XrControl` (`ctx.xr`): the top layer a game touches |
|
|
237
|
+
| `@thunderclap-engine/engine/core` | `GameLoop` (fixed step, fps cap, pause, `setFrames` to swap the frame source), `Scene` and `SceneStack`, `Emitter`, `mat4`, timing ([README](src/core/README.md)) |
|
|
238
|
+
| `@thunderclap-engine/engine/platform` | WebGPU boot, `Surface` (canvas, DPR, letterbox fit), `XrSurface` (a target and camera per eye), page lifecycle, device detection, the unsupported and reload screens ([README](src/platform/README.md)) |
|
|
239
|
+
| `@thunderclap-engine/engine/render` | `Renderer`, `Frame`, `Camera` (`ortho2d`, `perspective`, frustum culling), `render(frame, camera, target)`, `SceneTarget`, `Lights` (sun shadow map, fog), materials and custom WGSL, meshes with bounds and streaming updates, transforms, textures and texture arrays with GPU mips, colour helpers ([README](src/render/README.md)) |
|
|
240
|
+
| `@thunderclap-engine/engine/post` | `PostStack`: bloom, god rays, mist, vignette, grade, AO, DoF, finishing passes, TAA, SSR, auto exposure, white balance, motion blur, lens dirt, quality tiers ([README](src/post/README.md)) |
|
|
241
|
+
| `@thunderclap-engine/engine/fx` | GPU particles (2D or world space), ribbon trails, debug line drawing, billboards with a text atlas, projected decals, sky, flicker and pulse ([README](src/fx/README.md)) |
|
|
242
|
+
| `@thunderclap-engine/engine/anim` | Keyframe clips (glTF sampler semantics), `Pose`, `Skeleton`, `AnimationMixer` (layers, weights, crossfade, events), `poseToWorld`, `skinMatrices`, `twoBoneIk`, `lookAt`, root motion; pure, no GPU ([README](src/anim/README.md)) |
|
|
243
|
+
| `@thunderclap-engine/engine/tween` | `createTweens()`: easing table (`ease.*`, `steps`, `cubicBezier`, `spring`), property and value tweens, sequences and parallel groups, loops, yoyo, delays, `Tweens.update(dt)`; pure, no GPU ([README](src/tween/README.md)) |
|
|
244
|
+
| `@thunderclap-engine/engine/input` | `Input` action map, `Gestures`, pointer router and rays (`XrPointerSource` for controllers and hands), `Look` (pointer lock, touch zones, sticks), magnify/loupe, `snapToGrid` ([README](src/input/README.md)) |
|
|
245
|
+
| `@thunderclap-engine/engine/nav` | `createNavGrid` (edge costs, 4 or 8 neighbours), `findPath` (A*) and `smoothPath`, `createFlowField` (multi-goal, in-place update), `reachable`, `followPath`, `steer`: grid navigation over XZ, pure CPU ([README](src/nav/README.md)) |
|
|
246
|
+
| `@thunderclap-engine/engine/ui` | Overlay, panels and widgets, toasts, captions, pad navigation, layout regions (`ui.layout`), world anchors (`ui.anchors`) ([README](src/ui/README.md)) |
|
|
247
|
+
| `@thunderclap-engine/engine/audio` | WebAudio buses, synth, sound recipes, mixer persistence, spatial voices (`audio.listener`, `AudioPlayOptions.at`) ([README](src/audio/README.md)) |
|
|
248
|
+
| `@thunderclap-engine/engine/save` | `createSaveStore`, schemas (`s`, with typed arrays), migrations, `createSlotManager`, `createSettingsStore` with `bind`, `createJournal` ([README](src/save/README.md)) |
|
|
249
|
+
| `@thunderclap-engine/engine/assets` | `parseGltf` and `loadGltf` (glTF 2.0 to meshes, transforms, textures and materials), `defineManifest` and `createBundle` (typed asset loading from the game's `read`), `packAtlas` ([README](src/assets/README.md)) |
|
|
250
|
+
| `@thunderclap-engine/engine/analytics` | `createAnalytics`: typed, batched events to a game-supplied sink; no network code ([README](src/analytics/README.md)) |
|
|
251
|
+
| `@thunderclap-engine/engine/i18n` | `createI18n`: string tables per locale, ICU-style plurals and select through `Intl`, fallback chain, runtime locale swap with `subscribe`, `negotiateLocale` ([README](src/i18n/README.md)) |
|
|
252
|
+
| `@thunderclap-engine/engine/host` | `Host` interface, `createWebHost()` (top-level page or iframe), `createXrHost(session, base)` (frames from an `XRSession`): links, clipboard, fullscreen, storage ([README](src/host/README.md)) |
|
|
253
|
+
| `@thunderclap-engine/engine/voxel` | `createVoxelGrid`, `meshChunk` (greedy, baked AO), `createVoxelMesher` and `installVoxelMesherWorker` (the Worker bridge), `createVoxelChunks` (one mesh per chunk, rewritten in place, frustum-culled), `raycastVoxels`, the `VOXEL_WGSL` prelude ([README](src/voxel/README.md)) |
|
|
254
|
+
| `@thunderclap-engine/engine/testing` | Test doubles for unit tests (`src/testing/index.ts`): the fake GPU, a fake host and a fixed clock |
|
|
255
|
+
| `@thunderclap-engine/engine/geom` | Procedural geometry: `box`, `plane`, `sphere`, `icosphere`, `capsule`, `cylinder`, `torus`, heightfields (`heightAt`, `normalAt`, `raycastHeightfield`, `heightfieldGeometry`), `mergeGeometry`, `transformGeometry`; pure, a `Geometry` is a `MeshDesc` ([README](src/geom/README.md)) |
|
|
256
|
+
|
|
257
|
+
**Templates.** Copyable games under `templates/`, each with a `CLAUDE.md` that names the engine concepts for an agent extending it: `puzzle`, `platformer`, `first-person` and `third-person` (see [Quick start](#quick-start)). `pnpm template:<name>` runs one inside this repo. `pnpm check` runs each one's typecheck and tests and scaffolds one into a temp dir (`check:create-game`); `pnpm test:e2e` boots each on the device matrix (`e2e/templates.spec.ts`).
|
|
258
|
+
|
|
259
|
+
**Demos.** Playable showcases under `demos/` (E-021), each a page on the public API with no network assets, exposing `window.__demo` for its spec under `e2e/demos/`. `pnpm demo <name>` serves one (`pnpm demo` lists them); the harness server also serves them at `/demos/<name>/`, so the matrix runs them. [demos/README.md](demos/README.md) has the table.
|
|
260
|
+
|
|
261
|
+
**Packages.** `packages/host-electron` (`@thunderclap-engine/host-electron`) is the Electron desktop host: a sandboxed shell, a preload bridge and `createElectronHost()`. It lives in this repo as a pnpm workspace package and the engine never imports it. Run `pnpm desktop:dev`, `pnpm desktop:build` or `pnpm test:e2e:desktop` ([README](packages/host-electron/README.md)). `packages/host-capacitor` (`@thunderclap-engine/host-capacitor`) is the Capacitor mobile host: `createCapacitorHost()` fills `Host` from the Capacitor plugins an iOS or Android app hands in (links, clipboard, orientation lock, Preferences-backed saves, app lifecycle, haptics, the Android back button); it imports none of them, so it checks and tests in this repo without Capacitor installed (`check:host-capacitor`, [README](packages/host-capacitor/README.md)). `packages/host-pwa` (`@thunderclap-engine/host-pwa`) is the installable web shell: `createPwaHost()` adds install, update and offline state to the web host, and `pwaPlugin` (`@thunderclap-engine/host-pwa/vite`) emits the manifest and service worker at build time, with no dependencies. Run `pnpm pwa:dev` or `pnpm pwa:build` ([README](packages/host-pwa/README.md)).
|
|
262
|
+
|
|
263
|
+
[ARCHITECTURE.md](ARCHITECTURE.md) has the contracts for each one: what exists today and what is planned.
|
|
264
|
+
|
|
265
|
+
### Baking
|
|
266
|
+
|
|
267
|
+
A navmesh or a static collider is a pure function of its level geometry, so bake it once at build time and ship the bytes:
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
pnpm bake bake.config.json # writes <out>/<name>.navm, <out>/<name>.coll and bake-manifest.json
|
|
271
|
+
pnpm bake bake.config.json --check # exit 1 when any input changed since the last bake (CI)
|
|
272
|
+
pnpm bake bake.config.json --watch # rebake on change; --verbose prints sizes and times
|
|
273
|
+
pnpm bake:nav level.glb level.navm --options nav.json # one source to one navmesh, no config or manifest
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`bake.config.json` (paths relative to the file; `out` defaults to `public/baked`):
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{
|
|
280
|
+
"out": "public/baked",
|
|
281
|
+
"nav": [{ "name": "level", "sources": ["assets/level.glb"], "options": { "cellSize": 0.3, "agentHeight": 1.8, "agentRadius": 0.4 } }],
|
|
282
|
+
"colliders": [{ "name": "level", "sources": ["assets/level.glb", "assets/props.json"], "options": { "leafSize": 4 } }]
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
A source is a `.glb`, a `.gltf` or a geometry JSON (`{ "positions": [...], "indices": [...] }`). `options` are the `NavBakeOptions` (`nav`) or `StaticColliderOptions` (`geom`) the runtime takes; a navmesh needs only `cellSize` and a bad value fails with a `NavBakeError` naming the option. The game loads the bytes with `fetch` or a `bytes` bundle entry and calls `importNavMesh` or `importCollider` (`loadNavMesh` for a `bake:nav` file). From a game's folder the script is `node node_modules/@thunderclap-engine/engine/scripts/bake.mjs bake.config.json` (the third-person template's `pnpm bake`); it runs the engine's TypeScript through Node's type stripping, needs no build step, and its output is byte-identical between runs. The [nav reference](src/nav/README.md) has the option table.
|
|
287
|
+
|
|
288
|
+
## Testing
|
|
289
|
+
|
|
290
|
+
```sh
|
|
291
|
+
pnpm check # typecheck + Biome lint + import boundaries + README coverage (check:docs) + snippet and deprecation check (check:snippets) + API snapshots and the v1 freeze rule (check:api) + Vitest unit tests (2456 tests in 228 files) + the host-electron, host-capacitor, host-pwa, template, create-game and bake checks
|
|
292
|
+
pnpm test:e2e # Playwright e2e against the harness pages, on a device matrix
|
|
293
|
+
pnpm harness # serve the harness pages yourself
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The e2e specs run against small harness pages that don't contain any game (`harness/`). Every spec runs on the same matrix:
|
|
297
|
+
|
|
298
|
+
- desktop at 1280×720 and 1920×1080;
|
|
299
|
+
- an iPad;
|
|
300
|
+
- four landscape phones with touch and safe-area insets;
|
|
301
|
+
- the harness inside a cross-origin iframe.
|
|
302
|
+
|
|
303
|
+
The layout spec also runs on upright phone and tablet rows. The pages: `index.html` (2D renderer), `game.html` (`createGame`), `ui.html` (the overlay, text scale, menus and panels, no WebGPU), `layout.html` (layout regions and safe areas on every form factor, no WebGPU), `gestures.html` (gestures and the magnify loupe, no WebGPU), `widgets.html` (the second widget set: reticle, bar, segmented, select, key prompt, stepper and cooldown on the HUD layer, no WebGPU), `rays.html` (pointer router and rays), `mesh.html` (perspective camera, depth, a shadowing sun, fog and a lit mesh orbiting under pointer and pad input; `?still` parks the lamp and the spinning blocks for pixel probes), `lights.html` (a sweeping spot light and a point light masked to one of two cubes on different layers), `zones.html` (audio zones: the camera orbits through a sphere room and a box corridor while `audio.update` drives weights, ambient gains and reverb sends, and a voice follows an orbiting cube), `pbr.html` (the standard PBR material: a metallic × roughness sphere grid and a normal-mapped tile under sun, shadow and fog), `skin.html` (skinned tubes swung by a joint palette, lit and beside a shadow caster), `views.html` (the mesh scene drawn twice per frame through two eyes, the stereo seam), `weather.html` (world-space rain, snow and lit dust over the mesh scene), `fx3.html` (the extended particle kernel over the same scene, one mode per load: `?sheet` a procedural sprite sheet and a parked spinning sprite walking its tiles, `?forces` an attractor and vortex shadowed on the CPU, `?collide` rain staying on a floating slab through depth collision, `?sub` fireworks whose children outlive their parents, `?effect` the bundled aura attached to a moving point; `?still` parks the motion), `fx4.html` (seeded effects and decals on the depth hook, one mode per load: `?seed=N` a two-system effect seeded with N, stepped 120 fixed steps, frozen and hashed from a readback, the same on every load; `?decalLag` a cube sliding under a box decal that reads this frame's depth, no decal pixel off the cube; `©` the previous-frame fallback, where the decal trails), `sky.html` (a procedural `Sky` over the mesh scene's ground and blocks: the sun arcing through a day, stars at night, `?still` parks it at noon), `water.html` (a `Water` surface in a basin of boxes with a ramp running into it under a `Sky`: depth fade, foam, the sun's highlight; `?still` parks the sun), `decals.html` (projected decals stamped onto a floor and two blocks by click or api; `?mode=box|quad`), `trails.html` (ribbon trails behind a swinging arm and an orbiting emitter), `tween.html` (a grid of easing curves, each with a sprite `Tweens.to` moves along it, the spring included), `anim-graph.html` (a three-joint figure of boxes driven by `createAnimGraph`: an idle/walk/run blend space on `speed` with phase sync, a `jump` state on a boolean), `look.html` (a first-person walk on `Look`: pointer lock, touch zones, sticks, WASD), `follow.html` (a third-person `Follow` camera over `Look`: orbit, shoulder, walls swinging it in, `V` for first person), `stream.html` (chunk meshes rebuilt in a Worker and uploaded without a copy, textured from a texture array and frustum-culled), `voxel.html` (a 4×2×4-chunk block world through `@thunderclap-engine/engine/voxel`: meshed with baked AO in a Worker, culled by chunk, picked by the centre ray, a click breaks and the secondary button places, edits rewriting the chunk mesh in place; `?still` parks the sun), `anchors.html` (DOM labels and a clamped marker following cubes through `ctx.ui.anchors`), `hud.html` (the overlay's reticle, meters, slot strip and heading over the mesh scene), `gltf.html` (a GLB built in the page parsed and drawn through `@thunderclap-engine/engine/assets`), `gltf3.html` (ws/assets-3: glTF extensions on built GLBs, a quantized cube beside its float twin with `?quant`, a meshopt rock with `?meshopt`, a scene lit by its own punctual lights through its own camera with `?lights`, material variants cycling with `?variants`, ten assets through a small `createAssetCache` budget with `?cache`; `?still` parks), `morph.html` (a generated `.gltf` sphere with squash and bulge morph targets driven by its `weights` clip through `sampleWeights` and `scene.morph(node).apply()`), `xr.html` (`createGame({ xr })` with an "Enter VR" button; on the matrix it proves the page path with `supported() === false`, on a headset it enters an immersive session), `pick.html` (six spinning meshes picked through `meshTester`: click or tap tints the hit one), `retarget.html` (two stick figures, no WebGPU: a wave clip on one rig drives a longer-limbed rig with other joint names and rotated rest frames through `retargetPose`), `sockets.html` (a mixer-driven three-joint arm with cubes attached to its joints by index and by name through `attachTransform`), `geom.html` (every `@thunderclap-engine/engine/geom` builder turning, lit, over a heightfield ground from a sine bump map), `geom3.html` (ws/geom-3: `?lod` a rock at four distances drawn at the LOD level `selectLod` picks, `?hull` a torus beside its convex hull, `?slice` a cube cut and capped by a sweeping plane, `?tiles` a box riding a moving platform through `slideTiles`; `?still` parks), `i18n.html` (a locale switcher over `en`, `ru` and a lazily loaded `ar`, a plural counter and an RTL sample, no WebGPU), `ik.html` (a two-bone arm following an orbiting target and a head looking at it, through `twoBoneIk` and `lookAt`, no WebGPU), `save.html` (three save slots listed from their meta sidecars, a volume slider bound to a settings store, a 64×64 `Uint8Array` tilemap with undo and redo, no WebGPU), `music.html` (synth stems on a 120 bpm beat grid through `createMusic`, an intensity slider crossfading two drum stems, a footstep button on `createVariations`, a reverb send and a spoken line that ducks the music, no WebGPU), `depth-post.html` (the mesh scene, still, under screen-space ambient occlusion and depth of field; `?ao=0`, `?dof=0`), `post2.html` (a tilted flat-shaded cube over a gradient, still, with one finishing pass on: `?pass=fxaa|lut|grain|aberration|sharpen|outline`), `post3.html` (a lit mirror floor, a grey block and a bright sky patch over a dark backdrop under the post-3 passes: `?taa ?ssr ?autoexp ?wb=3200 ?mblur ?dirt`, `?still` parks the orbit), `post4.html` (the post stack on the render-4 inputs, each feature read back against a reference stack: `?taa` jittered convergence and a sliding bar's trail, `?ssr` a rough and a smooth metal floor half, `?blur` a cube swinging under a still camera, `?ortho` the AO term at two ortho zooms, `?still`), `nav.html` (a grid with a gapped wall, no WebGPU: a click sets the goal, 32 agents flow to it, a probe path is drawn after string pulling), `navmesh.html` (a level of `geom` boxes and a ramp baked into a `NavMesh` on the page and drawn with `createDebugDraw`: a click sets the goal, a path climbs the ramp or takes an off-mesh "jump down" link when it is cheaper, `?crowd=24` adds an ORCA crowd converging on the pointer), `nav4.html` (a floor and two pillars baked on the page, no WebGPU: 48 crowd agents on corridors, `&carve` dropping an obstacle between the pillars that replans them, `?queue` taking 200 requests a second through a path queue, `?areas` pricing mud, `?flow3` walking a mesh flow field; `?still`), `nav5.html` (ws/nav-5, no WebGPU: a unit's movement range on a tactics board of tiles with blocks and mud, `reachable(mesh, …)` filling the polygons in range and `reachableOutline` stroking its boundary loops, recomputed every step; `?range`, `?still`), `debug.html` (every `DebugDraw` shape in one line-list draw: axes over a grid, a spinning wire box and sphere, an arrow riding a sine path and a `path` from a static array), `billboards.html` (six capsules with name plates, health bars and rising damage numbers from one `Billboards` batch), `collide.html` (a level from the geom builders with a capsule walked by WASD or the stick through `slide`, the collider's triangles or BVH leaves as a wireframe; `?grid` a 2D corridor with a circle on `slide2`; `?still`), `tiles.html` (a 32×18 `TileLayer` from a generated checker atlas drawn as one call, with a `Flipbook` sprite walking across), `replay.html` (a sprite on WASD, arrows and the pad: a scripted 3 s session is recorded through `createInputRecorder`, replayed through `createInputPlayback` and compared tick by tick), `assets.html` (a 12-entry manifest of every kind loaded through `createBundle` from an in-memory `read`, a progress bar and the packed atlas drawn as sprites; `?slow`, `?fail=one`, `?cancel`), `inventory.html` (two item grids, a slot strip drop target, cards on focus and hover, a radial through `?radial` or a long press on the canvas and a context list on the secondary button, no WebGPU), `anim4.html` (ws/anim-4, no WebGPU: a stick figure under a 2D blend space, an aim offset, foot placement over a step, a spring cape and pose-matched cuts; `?blend2d`, `?aim`, `?foot`, `?spring`, `?match`), `audio4.html` (ws/audio-4: an occluded emitter behind a wall the listener orbits with `?occlude`, a passing Doppler emitter with `?doppler`, sprite clips of one buffer with `?sprites`, a streamed synthesised track with `?stream`, `&still` parks; no WebGPU), `input5.html` (ws/input-5: action contexts with `?contexts`, the input buffer with `?buffer`, the three stick shapes with `?stick`, gyro aim from a fake rotation rate with `?gyro`, `Look.onTap` with `?tap`, a two-thumb session recorded and replayed with `?replay`, `&still` parks; no WebGPU), `core2.html` (ws/core-2: two forked `createRandom` streams scattering 2000 points with `?random`, an fbm field of `createNoise` with `?noise`, `createScheduler` blinks and a cooldown bar with `?timer`, a scene push with a 1 s `preload` under `ui.loading` with `?preload`, `?still` parks), `world-panel.html` (ws/ui-world-panel: two wall panels in a `geom` room and one head-locked, every world widget, pressed by mouse and touch rays, a pad cursor with `?pad`; the panels ride the input tick through `attach(ctx.input)`), `ui4.html` (ws/ui-4, one mode per query over four lit blocks: `?floats` float numbers from the pooled `ui.floats`, `?ring` a filling `ui.ring`, `?compass` a `ui.compass` following an orbiting camera, `?minimap` a rotating `ui.minimap` with markers clamped to the rim, `?modal` a world cursor and `PadNav` handing the pad to a modal through `ui.padOwner`; `?still` parks the clock), `ui5.html` (ws/ui-5, one mode per query over the same blocks: `?orb` two orb meters draining with a damage trail, `?floats` floating text from a ring of points with crits and a label behind the camera culled, `?minimap` a round player-up canvas minimap over a painted background with 20 markers and click-to-pick; `?still` parks the clock), `render4.html` (ws/render-4: `?jitter` a quad under a two-sample camera jitter through a post processor that wants it, `?aux` a roughness gradient and a sliding cube into rough-metal and velocity targets read back through a debug blit, `?ranges` one mesh drawn through three index ranges, `?depth-test-off` a panel with `depthTest: false` inside a cube; `pbr.html?tangents` adds a tangent-layout tile), `render5.html` (ws/render-5: `?ktx2` a BC1 KTX2 file built in the page uploaded with its mips through `uploadKtx2` on a device created with the compression features, or the typed error without them, `?lod` 200 rocks of one `MeshLod` drawn one call per level, `?dissolve` a dissolving sphere whose shadow loses the same pixels; `?still` parks), `render7.html` (ws/render-7: `?lod-keys` 16 keyed rocks of one `MeshLod` swapped between slots every frame without a level change, `?instanced-velocity` a sliding and a standing row of rocks in one keyed list writing per-instance velocity, `&unkeyed` drops the keys, `?still` parks), `render10.html` (ws/render-10: sky, water, decals, particles, billboards and debug lines through `createGame({ aux })`, `?aux=velocity|all|none`, reporting no GPU error), `host.html` (the web host's facets at top level and inside the cross-origin iframe: detection, links and copy, fullscreen and save storage), `input.html` (orbit look, rebinding through `listen`, hold and chord triggers, device tracking and prompt glyphs) and `bench.html` (the `pnpm bench` scene ladder, below). That is 70 pages in `harness/`. The engine tests what the platform can do; a game built on it tests only its own product.
|
|
304
|
+
|
|
305
|
+
Headless Chromium gets a WebGPU adapter with `--enable-unsafe-webgpu`. On macOS that's Metal; on Linux it's SwiftShader. `playwright.config.ts` sets the flags.
|
|
306
|
+
|
|
307
|
+
**Snapshot gate.** "2D harness pixel-identical" is a command: `pnpm snap` captures every 2D harness page (`index.html` as `smoke`, `game.html`, `ui.html`, `layout.html`, `gestures.html`) on the `desktop-1280` row at a fixed frame into `e2e/golden/<page>.png` (committed), and `pnpm snap:check` captures them again and compares, failing on any pixel whose channel delta exceeds 1/255 (the E-008 tolerance) and writing a diff PNG to `screens/diff/<page>.png` (red beyond tolerance, yellow within it); the capture itself lands in `screens/golden/`. The same comparison runs inside `pnpm test:e2e` as `e2e/golden.spec.ts`, on that row only. `scripts/snap.mjs` holds the comparison (the PNG codec is the one Playwright bundles; the engine gains nothing) and also compares two files offline: `node scripts/snap.mjs compare a.png b.png [diff.png]`. A page can be golden-tested when its picture is a function of its frame count alone: the spec installs Playwright's fake clock before navigation (frozen `Date`, timers and `requestAnimationFrame`; `performance.now` quantised to the 16 ms frame grid), waits for `ready`, runs exactly 12 animation frames and screenshots, so a page must not read the wall clock outside its frame loop, use unseeded `Math.random` in anything it draws, or hold a perspective camera. Excluded, and why: `views.html` (two `Camera.perspective` eyes, spinning cubes) and the 3D pages `mesh`, `weather`, `trails`, `anim-graph`, `look`, `stream`, `voxel`, `anchors`, `hud`, `rays`, `gltf`, `gltf3`, `morph`, `xr` (perspective cameras, pointer-driven motion; each carries pixel probes in its own spec and is outside the E-008 invariant). When a brief changes what a 2D page draws, run `pnpm snap` and commit the new goldens with the change.
|
|
308
|
+
|
|
309
|
+
**Bench.** `pnpm bench` (`scripts/bench.mjs`) runs `harness/bench.html` in headless Chromium at the `desktop-1280` size through a fixed scene ladder (cube instances 1k/10k/50k with culling on and off, 0/8/16 point lights, the sun shadow on and off, 10k/50k sprites, 10k/100k world particles), holds each rung for 140 frames and records the median CPU ms (`performance.now()` around the render callback) and GPU ms (`renderer.gpuTimer`, `n/a` without `timestamp-query`) with `renderer.stats`. It prints the table and rewrites [`docs/BENCH.md`](docs/BENCH.md) with the machine line (user agent, adapter) and date; `--rung <id,id>` runs part of the ladder without writing. The numbers are what that machine measured at that moment, not a ceiling. `e2e/bench.spec.ts` runs one tiny rung on `desktop-1280` only.
|
|
310
|
+
|
|
311
|
+
## Status
|
|
312
|
+
|
|
313
|
+
Version **1.0.0**. Everything under [Features](#features) is on `main` and tested on the device matrix. The public surface in `api/*.api.txt` is the frozen `engine-v1.0.0` surface: the 0.x deprecated aliases are removed, and [Migrating to v1](docs/guide/migrating-to-v1.md) maps each old name to its replacement. Any later change to that surface needs a CHANGELOG entry. Read [CHANGELOG.md](CHANGELOG.md) before you upgrade.
|
|
314
|
+
Two games set the range it must cover: The Last Rune (2D) and Forgewilds (3D voxel), and each new capability arrives with a game that needs it ([VISION](docs/VISION.md)).
|
|
315
|
+
`@thunderclap-engine/engine` and the three hosts publish to npm from a `v*` tag ([Releasing](docs/releasing.md)). Current branches are on [docs/STATUS.md](docs/STATUS.md), the decisions behind the engine in [docs/DECISIONS.md](docs/DECISIONS.md).
|
|
316
|
+
The engine was extracted from [The Last Rune](https://github.com/Mandalorian007/last-rune) with its history, so `D-0xx` numbers and some `docs/` paths in comments point at that repo.
|
|
317
|
+
|
|
318
|
+
### Built with ThunderClap
|
|
319
|
+
|
|
320
|
+
#### The Last Rune
|
|
321
|
+
|
|
322
|
+
[**Play free in your browser →**](https://mandalorian007.github.io/last-rune/) · [Source](https://github.com/Mandalorian007/last-rune)
|
|
323
|
+
|
|
324
|
+
The Last Rune is the game that started ThunderClap, and the first one built on it.
|
|
325
|
+
|
|
326
|
+
Runes are gemstones that carry lightning. In a forgotten facility you set them into carved stone altars so a captured bolt flows from the storm cell to a dead machine and arrives at exactly full charge: not a spark short, not a spark over. There are sixteen machines and six kinds of gem, each with its own rule. Your guide is FUSE, a forty-year-old foreman robot who has been waiting a long time for someone to show up. It takes about an hour.
|
|
327
|
+
|
|
328
|
+
Everything on screen and every sound is procedural, generated in code with no asset files. The engine came out of building it. The renderer, bloom and god rays, the gem glow, the phone layout, the touch loupe for small sockets and the save system all began as Last Rune code, and moved into the engine once they no longer named anything from the game.
|
|
329
|
+
|
|
330
|
+
*Made a game with ThunderClap? Open a pull request and add it here.*
|
|
331
|
+
|
|
332
|
+
### License
|
|
333
|
+
|
|
334
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics: typed, batched events handed to a sink the game supplies. The engine ships no
|
|
3
|
+
* network sink (VISION rule 4): nothing here calls fetch or sendBeacon.
|
|
4
|
+
*/
|
|
5
|
+
import type { EventMap } from '../core/events.js';
|
|
6
|
+
import type { EngineCtx, HideReason } from '../core/game.js';
|
|
7
|
+
import type { HostKind } from '../host/types.js';
|
|
8
|
+
import type { EmbedContext } from '../platform/links.js';
|
|
9
|
+
import { type FormFactor } from '../ui/layout.js';
|
|
10
|
+
export type SessionEndReason = HideReason | 'deviceLost';
|
|
11
|
+
/** Props of the events the engine emits itself. */
|
|
12
|
+
export interface EngineEvents {
|
|
13
|
+
game_start: {
|
|
14
|
+
formFactor: FormFactor;
|
|
15
|
+
deviceKind: HostKind;
|
|
16
|
+
dpr: number;
|
|
17
|
+
viewport: {
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
};
|
|
21
|
+
embed: EmbedContext;
|
|
22
|
+
/** The build or version string the game passed in `context.build`. */
|
|
23
|
+
build: string | null;
|
|
24
|
+
};
|
|
25
|
+
session_end: {
|
|
26
|
+
durationMs: number;
|
|
27
|
+
reason: SessionEndReason;
|
|
28
|
+
};
|
|
29
|
+
device_lost: {
|
|
30
|
+
reason: string;
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Values a game may attach to every event (build, channel, experiment arm). */
|
|
35
|
+
export type AnalyticsContext = Readonly<Record<string, string | number | boolean>>;
|
|
36
|
+
export interface AnalyticsEvent<N extends string = string, P = unknown> {
|
|
37
|
+
readonly name: N;
|
|
38
|
+
readonly props: P;
|
|
39
|
+
/** Random per page load. */
|
|
40
|
+
readonly session: string;
|
|
41
|
+
/** Monotonic per session, from 1. */
|
|
42
|
+
readonly seq: number;
|
|
43
|
+
/** `clock()` at emit, ms. */
|
|
44
|
+
readonly t: number;
|
|
45
|
+
readonly context?: AnalyticsContext;
|
|
46
|
+
}
|
|
47
|
+
export type AnalyticsSink = (events: AnalyticsEvent[]) => void | Promise<void>;
|
|
48
|
+
export interface AnalyticsOptions {
|
|
49
|
+
/** Receives each batch. The game owns delivery (see the README for a sendBeacon sink). */
|
|
50
|
+
sink: AnalyticsSink;
|
|
51
|
+
/** Session id. Default: random. */
|
|
52
|
+
session?: string;
|
|
53
|
+
/** Time source, ms. Default `Date.now`. */
|
|
54
|
+
clock?: () => number;
|
|
55
|
+
/** Attached to every event. `build` also lands in `game_start`. */
|
|
56
|
+
context?: AnalyticsContext;
|
|
57
|
+
/** A sink failure. Never thrown. */
|
|
58
|
+
onError?: (error: unknown) => void;
|
|
59
|
+
/** Flush on its own when this many events are queued. Default 20; 0 disables. */
|
|
60
|
+
maxBatch?: number;
|
|
61
|
+
}
|
|
62
|
+
type Args<M, K extends keyof M> = M[K] extends undefined ? [props?: M[K]] : [props: M[K]];
|
|
63
|
+
export interface Analytics<E extends EventMap = EventMap> {
|
|
64
|
+
readonly session: string;
|
|
65
|
+
/** Events queued and not yet flushed. */
|
|
66
|
+
readonly pending: number;
|
|
67
|
+
emit<K extends keyof (E & EngineEvents) & string>(name: K, ...props: Args<E & EngineEvents, K>): void;
|
|
68
|
+
/** Hands the queue to the sink. Resolves when the sink does; never rejects. */
|
|
69
|
+
flush(): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Emits `game_start`, then `session_end` and a flush when the page hides, and `device_lost`
|
|
72
|
+
* when the GPU goes. Wraps `ctx.hooks.hide`, `show` and `deviceLost`, keeping the game's own.
|
|
73
|
+
* Returns detach.
|
|
74
|
+
*/
|
|
75
|
+
attach(ctx: AttachTarget): () => void;
|
|
76
|
+
}
|
|
77
|
+
/** The part of `EngineCtx` `attach` reads. */
|
|
78
|
+
export type AttachTarget = Pick<EngineCtx, 'hooks' | 'host'> & Partial<Pick<EngineCtx, 'ui' | 'surface'>>;
|
|
79
|
+
export declare function createAnalytics<E extends EventMap = EventMap>(opts: AnalyticsOptions): Analytics<E>;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { classifyFormFactor } from '../ui/layout.js';
|
|
2
|
+
function randomId() {
|
|
3
|
+
const c = globalThis.crypto;
|
|
4
|
+
if (c?.randomUUID)
|
|
5
|
+
return c.randomUUID();
|
|
6
|
+
let s = '';
|
|
7
|
+
for (let i = 0; i < 16; i++)
|
|
8
|
+
s += Math.floor(Math.random() * 16).toString(16);
|
|
9
|
+
return s;
|
|
10
|
+
}
|
|
11
|
+
export function createAnalytics(opts) {
|
|
12
|
+
const clock = opts.clock ?? Date.now;
|
|
13
|
+
const session = opts.session ?? randomId();
|
|
14
|
+
const maxBatch = opts.maxBatch ?? 20;
|
|
15
|
+
const report = (e) => {
|
|
16
|
+
try {
|
|
17
|
+
opts.onError?.(e);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
/* the error handler must not throw either */
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
let queue = [];
|
|
24
|
+
let seq = 0;
|
|
25
|
+
let flushing = Promise.resolve();
|
|
26
|
+
const flush = () => {
|
|
27
|
+
if (!queue.length)
|
|
28
|
+
return flushing;
|
|
29
|
+
const batch = queue;
|
|
30
|
+
queue = [];
|
|
31
|
+
// The sink runs now (a page that is hiding may not get another turn).
|
|
32
|
+
let p;
|
|
33
|
+
try {
|
|
34
|
+
p = Promise.resolve(opts.sink(batch)).catch(report);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
report(e);
|
|
38
|
+
p = Promise.resolve();
|
|
39
|
+
}
|
|
40
|
+
flushing = Promise.all([flushing, p]).then(() => { });
|
|
41
|
+
return flushing;
|
|
42
|
+
};
|
|
43
|
+
const emit = (name, props) => {
|
|
44
|
+
try {
|
|
45
|
+
queue.push({
|
|
46
|
+
name,
|
|
47
|
+
props: props ?? {},
|
|
48
|
+
session,
|
|
49
|
+
seq: ++seq,
|
|
50
|
+
t: clock(),
|
|
51
|
+
...(opts.context ? { context: opts.context } : {}),
|
|
52
|
+
});
|
|
53
|
+
if (maxBatch > 0 && queue.length >= maxBatch)
|
|
54
|
+
void flush();
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
report(e);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const attach = (ctx) => {
|
|
61
|
+
const start = clock();
|
|
62
|
+
const win = typeof window === 'undefined' ? null : window;
|
|
63
|
+
const coarse = !!win?.matchMedia?.('(pointer: coarse)')?.matches;
|
|
64
|
+
const width = win?.innerWidth ?? 0;
|
|
65
|
+
const height = win?.innerHeight ?? 0;
|
|
66
|
+
const formFactor = ctx.ui?.layout.formFactor ??
|
|
67
|
+
classifyFormFactor({
|
|
68
|
+
width,
|
|
69
|
+
height,
|
|
70
|
+
coarsePointer: coarse,
|
|
71
|
+
shortSide: Math.min(win?.screen?.width ?? 0, win?.screen?.height ?? 0),
|
|
72
|
+
});
|
|
73
|
+
emit('game_start', {
|
|
74
|
+
formFactor,
|
|
75
|
+
deviceKind: ctx.host.kind,
|
|
76
|
+
dpr: win?.devicePixelRatio ?? 1,
|
|
77
|
+
viewport: { width, height },
|
|
78
|
+
embed: ctx.host.embed,
|
|
79
|
+
build: typeof opts.context?.build === 'string' ? opts.context.build : null,
|
|
80
|
+
});
|
|
81
|
+
// hidden, pagehide and freeze can fire in a row: one session_end per hidden stretch.
|
|
82
|
+
let ended = false;
|
|
83
|
+
const end = (reason) => {
|
|
84
|
+
if (ended)
|
|
85
|
+
return;
|
|
86
|
+
ended = true;
|
|
87
|
+
emit('session_end', { durationMs: clock() - start, reason });
|
|
88
|
+
void flush();
|
|
89
|
+
};
|
|
90
|
+
const hooks = ctx.hooks;
|
|
91
|
+
const prevHide = hooks.hide;
|
|
92
|
+
const prevShow = hooks.show;
|
|
93
|
+
const prevLost = hooks.deviceLost;
|
|
94
|
+
const hide = (reason) => {
|
|
95
|
+
end(reason);
|
|
96
|
+
prevHide?.call(hooks, reason);
|
|
97
|
+
};
|
|
98
|
+
const show = (reason) => {
|
|
99
|
+
ended = false;
|
|
100
|
+
prevShow?.call(hooks, reason);
|
|
101
|
+
};
|
|
102
|
+
const lost = (info) => {
|
|
103
|
+
emit('device_lost', { reason: String(info.reason), message: String(info.message) });
|
|
104
|
+
end('deviceLost');
|
|
105
|
+
prevLost?.call(hooks, info);
|
|
106
|
+
};
|
|
107
|
+
hooks.hide = hide;
|
|
108
|
+
hooks.show = show;
|
|
109
|
+
hooks.deviceLost = lost;
|
|
110
|
+
return () => {
|
|
111
|
+
// Restore only if nobody wrapped us since; otherwise the chain stays.
|
|
112
|
+
if (hooks.hide === hide)
|
|
113
|
+
hooks.hide = prevHide;
|
|
114
|
+
if (hooks.show === show)
|
|
115
|
+
hooks.show = prevShow;
|
|
116
|
+
if (hooks.deviceLost === lost)
|
|
117
|
+
hooks.deviceLost = prevLost;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
session,
|
|
122
|
+
get pending() {
|
|
123
|
+
return queue.length;
|
|
124
|
+
},
|
|
125
|
+
emit: emit,
|
|
126
|
+
flush,
|
|
127
|
+
attach,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type Analytics, type AnalyticsContext, type AnalyticsEvent, type AnalyticsOptions, type AnalyticsSink, type AttachTarget, createAnalytics, type EngineEvents, type SessionEndReason, } from './analytics.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createAnalytics, } from './analytics.js';
|