@waica/engine 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +79 -1
  2. package/dist/anchored-pieces.d.ts +3 -2
  3. package/dist/anchored-pieces.js +12 -8
  4. package/dist/archetype.d.ts +2 -2
  5. package/dist/assets/asset-loader.d.ts +28 -5
  6. package/dist/assets/asset-loader.js +63 -7
  7. package/dist/assets/model-backend.d.ts +21 -0
  8. package/dist/assets/model-backend.js +21 -0
  9. package/dist/assets/model-cache.d.ts +47 -0
  10. package/dist/assets/model-cache.js +117 -0
  11. package/dist/audio/audio-subsystem.d.ts +6 -1
  12. package/dist/audio/audio-subsystem.js +8 -15
  13. package/dist/audio/placement.d.ts +41 -0
  14. package/dist/audio/placement.js +21 -0
  15. package/dist/audio/spatial.d.ts +20 -0
  16. package/dist/audio/spatial.js +26 -0
  17. package/dist/audio/types.d.ts +7 -1
  18. package/dist/camera-projection.d.ts +38 -0
  19. package/dist/camera-projection.js +44 -0
  20. package/dist/camera-rig.d.ts +48 -0
  21. package/dist/camera-rig.js +98 -0
  22. package/dist/camera.d.ts +20 -3
  23. package/dist/camera.js +3 -1
  24. package/dist/component.d.ts +38 -2
  25. package/dist/component.js +3 -0
  26. package/dist/components/animated-sprite.d.ts +8 -1
  27. package/dist/components/animated-sprite.js +16 -1
  28. package/dist/components/collider.d.ts +89 -0
  29. package/dist/components/collider.js +57 -0
  30. package/dist/components/dynamic-body.d.ts +2 -1
  31. package/dist/components/dynamic-body.js +1 -0
  32. package/dist/components/hitbox.d.ts +2 -1
  33. package/dist/components/hitbox.js +1 -0
  34. package/dist/components/light.d.ts +69 -0
  35. package/dist/components/light.js +60 -0
  36. package/dist/components/model.d.ts +63 -0
  37. package/dist/components/model.js +117 -0
  38. package/dist/components/particle-emitter.d.ts +8 -1
  39. package/dist/components/particle-emitter.js +14 -0
  40. package/dist/components/point-light.d.ts +75 -0
  41. package/dist/components/point-light.js +102 -0
  42. package/dist/components/rigid-body.d.ts +114 -0
  43. package/dist/components/rigid-body.js +83 -0
  44. package/dist/components/solid.d.ts +2 -1
  45. package/dist/components/solid.js +1 -0
  46. package/dist/components/sprite.d.ts +8 -1
  47. package/dist/components/sprite.js +16 -1
  48. package/dist/components/sun.d.ts +46 -0
  49. package/dist/components/sun.js +74 -0
  50. package/dist/components/tilemap.d.ts +2 -1
  51. package/dist/components/tilemap.js +7 -0
  52. package/dist/entity-transform.d.ts +18 -0
  53. package/dist/entity-transform.js +17 -0
  54. package/dist/frame-quad.d.ts +21 -0
  55. package/dist/frame-quad.js +50 -0
  56. package/dist/game.d.ts +59 -15
  57. package/dist/game.js +100 -75
  58. package/dist/index.d.ts +39 -4
  59. package/dist/index.js +18 -1
  60. package/dist/light-field.d.ts +78 -0
  61. package/dist/light-field.js +145 -0
  62. package/dist/light-map-material.d.ts +40 -0
  63. package/dist/light-map-material.js +203 -0
  64. package/dist/light-map.d.ts +46 -0
  65. package/dist/light-map.js +132 -0
  66. package/dist/lit-frame.d.ts +48 -0
  67. package/dist/lit-frame.js +174 -0
  68. package/dist/occluder-grid.d.ts +19 -0
  69. package/dist/occluder-grid.js +50 -0
  70. package/dist/particle-batch.js +3 -2
  71. package/dist/physics-3d/body-contacts.d.ts +10 -0
  72. package/dist/physics-3d/body-contacts.js +59 -0
  73. package/dist/physics-3d/body-sync.d.ts +52 -0
  74. package/dist/physics-3d/body-sync.js +153 -0
  75. package/dist/physics-3d/character-controller.d.ts +44 -0
  76. package/dist/physics-3d/character-controller.js +126 -0
  77. package/dist/physics-3d/physics-host.d.ts +86 -0
  78. package/dist/physics-3d/physics-host.js +174 -0
  79. package/dist/physics-3d/physics-world.d.ts +65 -0
  80. package/dist/physics-3d/physics-world.js +158 -0
  81. package/dist/physics-3d/rapier-module.d.ts +17 -0
  82. package/dist/physics-3d/rapier-module.js +19 -0
  83. package/dist/physics-3d/sensor-dispatch.d.ts +10 -0
  84. package/dist/physics-3d/sensor-dispatch.js +77 -0
  85. package/dist/physics-3d/world-queries.d.ts +39 -0
  86. package/dist/physics-3d/world-queries.js +63 -0
  87. package/dist/pointer-raycast.d.ts +17 -0
  88. package/dist/pointer-raycast.js +48 -0
  89. package/dist/pointer.d.ts +27 -6
  90. package/dist/pointer.js +43 -17
  91. package/dist/post-effects.d.ts +33 -0
  92. package/dist/post-effects.js +48 -0
  93. package/dist/post-pass.d.ts +23 -0
  94. package/dist/post-pass.js +93 -0
  95. package/dist/render-layers.d.ts +18 -0
  96. package/dist/render-layers.js +19 -0
  97. package/dist/render-output.d.ts +7 -0
  98. package/dist/render-output.js +17 -1
  99. package/dist/runtime-bridge.d.ts +1 -1
  100. package/dist/runtime-inspection.d.ts +14 -0
  101. package/dist/runtime-inspection.js +12 -2
  102. package/dist/runtime-lighting-snapshot.d.ts +67 -0
  103. package/dist/runtime-lighting-snapshot.js +77 -0
  104. package/dist/runtime-physics-snapshot.d.ts +41 -0
  105. package/dist/runtime-physics-snapshot.js +45 -0
  106. package/dist/runtime-view-snapshot.d.ts +25 -0
  107. package/dist/runtime-view-snapshot.js +30 -0
  108. package/dist/scene-camera-3d.d.ts +66 -0
  109. package/dist/scene-camera-3d.js +117 -0
  110. package/dist/scene-lighting.d.ts +52 -0
  111. package/dist/scene-lighting.js +107 -0
  112. package/dist/scene-lights-3d.d.ts +17 -0
  113. package/dist/scene-lights-3d.js +27 -0
  114. package/dist/scene-render-options.d.ts +51 -0
  115. package/dist/scene-render-options.js +126 -0
  116. package/dist/scene-simulation.d.ts +22 -0
  117. package/dist/scene-simulation.js +29 -0
  118. package/dist/scene-space.d.ts +50 -0
  119. package/dist/scene-space.js +103 -0
  120. package/dist/scene.d.ts +41 -2
  121. package/dist/scene.js +11 -4
  122. package/dist/simulation-phases.d.ts +9 -0
  123. package/dist/simulation-phases.js +19 -0
  124. package/dist/spatial-query-3d.d.ts +48 -0
  125. package/dist/spatial-query-3d.js +96 -0
  126. package/dist/spatial-query.d.ts +18 -4
  127. package/dist/spatial-query.js +57 -1
  128. package/dist/sprite-batch.js +5 -1
  129. package/package.json +2 -1
@@ -0,0 +1,48 @@
1
+ import { resolveColorGrade, resolvePostEffects, resolveVignette, } from './scene-render-options.js';
2
+ /**
3
+ * `game.post` (issue #78): the live scene's Post Effects — a vignette and a
4
+ * color grade, each off (null) unless its scene declares it or a game turns
5
+ * it on. With either on, the frame renders through a render target and one
6
+ * full-screen pass; with both off, it draws straight to the canvas. Camera
7
+ * Effects (ADR 0020) sit above and are never altered. They die with their scene.
8
+ */
9
+ export class GamePost {
10
+ state = resolvePostEffects(undefined);
11
+ /** The vignette now, a copy; null when off. */
12
+ get vignette() {
13
+ const { vignette } = this.state;
14
+ return vignette ? { ...vignette } : null;
15
+ }
16
+ /**
17
+ * Turns the vignette off with null, or changes it field by field over the
18
+ * current one (absent fields take their defaults when it was off), clamped to 0..1.
19
+ */
20
+ set vignette(value) {
21
+ this.state = { ...this.state, vignette: value ? resolveVignette({ ...this.state.vignette, ...value }) : null };
22
+ }
23
+ /** The color grade now, a copy; null when off. */
24
+ get colorGrade() {
25
+ const { colorGrade } = this.state;
26
+ return colorGrade ? { ...colorGrade } : null;
27
+ }
28
+ /**
29
+ * Turns the color grade off with null, or changes it field by field over the
30
+ * current one (absent fields neutral when it was off), clamped — like
31
+ * `game.lighting.ambient`, so a tween of one field keeps the others.
32
+ */
33
+ set colorGrade(value) {
34
+ this.state = { ...this.state, colorGrade: value ? resolveColorGrade({ ...this.state.colorGrade, ...value }) : null };
35
+ }
36
+ /** Whether any Post Effect is on. */
37
+ get active() {
38
+ return this.state.vignette !== null || this.state.colorGrade !== null;
39
+ }
40
+ /** Adopts a scene's `render.post`, replacing whatever the previous scene left. Called by Game. */
41
+ loadScene(render) {
42
+ this.state = resolvePostEffects(render?.post);
43
+ }
44
+ /** Every effect off again. */
45
+ unloadScene() {
46
+ this.loadScene(undefined);
47
+ }
48
+ }
@@ -0,0 +1,23 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import type { PostEffectsState } from './scene-render-options.js';
3
+ /**
4
+ * The Post Effects' render target and pass: the frame is drawn into `target`
5
+ * exactly as onto the canvas — 8-bit, sRGB-encoded, blending on sRGB values
6
+ * (ADR 0025's canvas path) — at the light-map resolution with nearest upscale
7
+ * (E2), then `draw` writes it to the canvas through the effects. A half-float
8
+ * linear target drew nothing on CI's headless Linux WebGPU (PR #150), and it
9
+ * changed every translucent edge.
10
+ */
11
+ export declare class PostPass {
12
+ readonly target: THREE.RenderTarget<THREE.Texture<unknown, THREE.TextureEventMap>, THREE.RenderTargetEventMap>;
13
+ private readonly uniforms;
14
+ private readonly drawnLikeCanvas;
15
+ private readonly quad;
16
+ /** Sizes the target for this frame. */
17
+ resize(width: number, height: number): void;
18
+ /** Draws the target to whatever is bound now (the canvas), with this frame's effects. */
19
+ draw(renderer: THREE.WebGPURenderer, effects: PostEffectsState): void;
20
+ private setVignette;
21
+ private setColorGrade;
22
+ dispose(): void;
23
+ }
@@ -0,0 +1,93 @@
1
+ import { clamp, dot, float, length, max, mix, texture, uniform, uv, vec3, vec4 } from 'three/tsl';
2
+ import * as THREE from 'three/webgpu';
3
+ import { FrameQuad } from './frame-quad.js';
4
+ import { hexChannels } from './scene-lighting.js';
5
+ import { drawLikeCanvas } from './render-output.js';
6
+ function createUniforms() {
7
+ return {
8
+ vignetteOn: uniform(0),
9
+ vignetteIntensity: uniform(0),
10
+ vignetteRadius: uniform(0),
11
+ gradeOn: uniform(0),
12
+ tint: uniform(new THREE.Vector3(1, 1, 1)),
13
+ contrast: uniform(1),
14
+ saturation: uniform(1),
15
+ };
16
+ }
17
+ /**
18
+ * The single full-screen pass of the Post Effects (issue #78 CA-11): it reads
19
+ * the frame from the target, already in the canvas's sRGB values, grades it —
20
+ * tint, then saturation around Rec. 709 luma, then contrast around mid grey —
21
+ * and darkens it towards the corners. Its output is the canvas's sRGB value.
22
+ */
23
+ function createPostMaterial(scene, u) {
24
+ const material = new THREE.MeshBasicNodeMaterial();
25
+ // Already the canvas's sRGB values (drawLikeCanvas): graded as they would show.
26
+ const display = texture(scene, uv()).rgb;
27
+ const tinted = display.mul(u.tint);
28
+ const luma = dot(tinted, vec3(0.2126, 0.7152, 0.0722));
29
+ const saturated = mix(vec3(luma, luma, luma), tinted, u.saturation);
30
+ const graded = saturated.sub(0.5).mul(u.contrast).add(0.5);
31
+ const color = mix(display, graded, u.gradeOn);
32
+ // 0 at the centre, 1 at the corners.
33
+ const distance = length(uv().sub(0.5).mul(2)).div(Math.SQRT2);
34
+ const ramp = clamp(distance.sub(u.vignetteRadius).div(max(float(1).sub(u.vignetteRadius), 1e-3)), 0, 1);
35
+ const eased = ramp.mul(ramp).mul(float(3).sub(ramp.mul(2)));
36
+ const vignette = float(1).sub(u.vignetteIntensity.mul(eased).mul(u.vignetteOn));
37
+ material.fragmentNode = vec4(clamp(color.mul(vignette), vec3(0, 0, 0), vec3(1, 1, 1)), 1);
38
+ material.depthTest = false;
39
+ material.depthWrite = false;
40
+ return material;
41
+ }
42
+ /**
43
+ * The Post Effects' render target and pass: the frame is drawn into `target`
44
+ * exactly as onto the canvas — 8-bit, sRGB-encoded, blending on sRGB values
45
+ * (ADR 0025's canvas path) — at the light-map resolution with nearest upscale
46
+ * (E2), then `draw` writes it to the canvas through the effects. A half-float
47
+ * linear target drew nothing on CI's headless Linux WebGPU (PR #150), and it
48
+ * changed every translucent edge.
49
+ */
50
+ export class PostPass {
51
+ target = new THREE.RenderTarget(1, 1, {
52
+ type: THREE.UnsignedByteType,
53
+ minFilter: THREE.NearestFilter,
54
+ magFilter: THREE.NearestFilter,
55
+ depthBuffer: true,
56
+ // The canvas's own MSAA (antialias: true): geometry edges match the frame without post.
57
+ samples: 4,
58
+ });
59
+ uniforms = createUniforms();
60
+ drawnLikeCanvas = drawLikeCanvas(this.target);
61
+ quad = new FrameQuad(createPostMaterial(this.target.texture, this.uniforms));
62
+ /** Sizes the target for this frame. */
63
+ resize(width, height) {
64
+ if (this.target.width !== width || this.target.height !== height)
65
+ this.target.setSize(width, height);
66
+ }
67
+ /** Draws the target to whatever is bound now (the canvas), with this frame's effects. */
68
+ draw(renderer, effects) {
69
+ this.setVignette(effects.vignette);
70
+ this.setColorGrade(effects.colorGrade);
71
+ this.quad.render(renderer);
72
+ }
73
+ setVignette(vignette) {
74
+ const u = this.uniforms;
75
+ u.vignetteOn.value = vignette ? 1 : 0;
76
+ u.vignetteIntensity.value = vignette ? vignette.intensity : 0;
77
+ u.vignetteRadius.value = vignette ? vignette.radius : 0;
78
+ }
79
+ setColorGrade(grade) {
80
+ const u = this.uniforms;
81
+ const neutral = { tint: '#ffffff', contrast: 1, saturation: 1 };
82
+ const { tint, contrast, saturation } = grade ?? neutral;
83
+ u.gradeOn.value = grade ? 1 : 0;
84
+ const [r, g, b] = hexChannels(tint);
85
+ u.tint.value.set(r, g, b);
86
+ u.contrast.value = contrast;
87
+ u.saturation.value = saturation;
88
+ }
89
+ dispose() {
90
+ this.target.dispose();
91
+ this.quad.dispose();
92
+ }
93
+ }
@@ -0,0 +1,18 @@
1
+ import type * as THREE from 'three/webgpu';
2
+ /**
3
+ * The three.js layer an Emissive drawable lives on, alone (issue #78 CA-10).
4
+ * The Game's camera sees layer 0 and this one, so an unlit scene draws an
5
+ * Emissive drawable in place like any other; a lit scene draws layer 0, then
6
+ * its light-map, then this layer at full brightness (ADR 0026).
7
+ */
8
+ export declare const EMISSIVE_LAYER = 1;
9
+ /** Puts `object` on the Emissive layer alone, or back on the default layer 0. */
10
+ export declare function setEmissive(object: THREE.Object3D, emissive: boolean): void;
11
+ /**
12
+ * The alphaTest every drawable carries (PR #150 review): a texel whose alpha
13
+ * byte is 0 is discarded instead of drawn. It contributes no color either way
14
+ * (normal blending keeps the frame), but discarded it also writes no depth, so
15
+ * an Emissive drawable drawn after the lit pass shows through the transparent
16
+ * texels of a lit sprite in front of it, and stays hidden behind its opaque ones.
17
+ */
18
+ export declare const TRANSPARENT_TEXEL_ALPHA: number;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The three.js layer an Emissive drawable lives on, alone (issue #78 CA-10).
3
+ * The Game's camera sees layer 0 and this one, so an unlit scene draws an
4
+ * Emissive drawable in place like any other; a lit scene draws layer 0, then
5
+ * its light-map, then this layer at full brightness (ADR 0026).
6
+ */
7
+ export const EMISSIVE_LAYER = 1;
8
+ /** Puts `object` on the Emissive layer alone, or back on the default layer 0. */
9
+ export function setEmissive(object, emissive) {
10
+ object.layers.set(emissive ? EMISSIVE_LAYER : 0);
11
+ }
12
+ /**
13
+ * The alphaTest every drawable carries (PR #150 review): a texel whose alpha
14
+ * byte is 0 is discarded instead of drawn. It contributes no color either way
15
+ * (normal blending keeps the frame), but discarded it also writes no depth, so
16
+ * an Emissive drawable drawn after the lit pass shows through the transparent
17
+ * texels of a lit sprite in front of it, and stays hidden behind its opaque ones.
18
+ */
19
+ export const TRANSPARENT_TEXEL_ALPHA = 1 / 512;
@@ -8,6 +8,13 @@ import * as THREE from 'three/webgpu';
8
8
  * one more draw per frame. A render target keeps the linear working space.
9
9
  */
10
10
  export declare function drawStraightToCanvas(renderer: THREE.WebGPURenderer): void;
11
+ /**
12
+ * Makes `target` receive what the canvas would (issue #78): every material
13
+ * encodes to sRGB into it and blends on sRGB values, so a frame drawn there
14
+ * and copied to the canvas is the frame drawn straight to the canvas. The
15
+ * Post Effects' 8-bit target.
16
+ */
17
+ export declare function drawLikeCanvas(target: THREE.RenderTarget): THREE.RenderTarget;
11
18
  /**
12
19
  * The scene background for a canvas drawn straight: three clears with a
13
20
  * color's working-space (linear) components, and the canvas stores sRGB, so
@@ -12,7 +12,23 @@ export function drawStraightToCanvas(renderer) {
12
12
  // The working space as output: three then needs no intermediate target.
13
13
  renderer.outputColorSpace = THREE.ColorManagement.workingColorSpace;
14
14
  const context = renderer.contextNode.value;
15
- context.getOutput = (output, builder) => builder.renderer.getRenderTarget() === null ? workingToColorSpace(output, THREE.SRGBColorSpace) : output;
15
+ context.getOutput = (output, builder) => {
16
+ const target = builder.renderer.getRenderTarget();
17
+ const likeCanvas = target === null || (typeof target === 'object' && canvasLike.has(target));
18
+ return likeCanvas ? workingToColorSpace(output, THREE.SRGBColorSpace) : output;
19
+ };
20
+ }
21
+ /** Render targets that hold a frame exactly as the canvas would: sRGB-encoded 8-bit values. */
22
+ const canvasLike = new WeakSet();
23
+ /**
24
+ * Makes `target` receive what the canvas would (issue #78): every material
25
+ * encodes to sRGB into it and blends on sRGB values, so a frame drawn there
26
+ * and copied to the canvas is the frame drawn straight to the canvas. The
27
+ * Post Effects' 8-bit target.
28
+ */
29
+ export function drawLikeCanvas(target) {
30
+ canvasLike.add(target);
31
+ return target;
16
32
  }
17
33
  /**
18
34
  * The scene background for a canvas drawn straight: three clears with a
@@ -85,7 +85,7 @@ export interface RuntimeBridge {
85
85
  }
86
86
  /** Why a Game can never become operational; reported through the activation. */
87
87
  export interface RuntimeBridgeFailure {
88
- code: 'render-backend-failed';
88
+ code: 'render-backend-failed' | 'physics-backend-failed';
89
89
  message: string;
90
90
  }
91
91
  /** Ephemeral pre-page hook installed by the owner of a browser context. */
@@ -1,8 +1,12 @@
1
1
  import type { AudioChannelState, LiveSoundInfo } from './audio/types.js';
2
2
  import type { CameraEffectsState } from './camera-effects.js';
3
+ import type { SceneSpace } from './scene-space.js';
4
+ import { type RuntimeSnapshotView } from './runtime-view-snapshot.js';
3
5
  import type { Game } from './game.js';
4
6
  import type { RuntimeMetadata } from './runtime-bridge.js';
5
7
  import type { StatValue } from './stats.js';
8
+ import { type RuntimeSnapshotLighting, type RuntimeSnapshotPost } from './runtime-lighting-snapshot.js';
9
+ import { type RuntimeSnapshotPhysics } from './runtime-physics-snapshot.js';
6
10
  export type ProjectionMarkerKind = 'cycle' | 'unsupported' | 'error' | 'truncated';
7
11
  export interface ProjectionMarker {
8
12
  $waica: ProjectionMarkerKind | 'date' | 'bigint' | 'map' | 'set';
@@ -124,6 +128,16 @@ export interface RuntimeSnapshot extends RuntimeMetadata {
124
128
  time: RuntimeSnapshotTime;
125
129
  ui: RuntimeSnapshotUi;
126
130
  camera: RuntimeSnapshotCamera;
131
+ /** The live scene's space (`render.space`, ADR 0027); `'2d'` with no scene. Never filtered. */
132
+ space: SceneSpace;
133
+ /** The view the scene is drawn from: the orthographic centre and zoom, or the perspective pose (issue #154 CA-16). Never filtered. */
134
+ view: RuntimeSnapshotView;
135
+ /** Ambient Light and live Lights (issue #78 CA-12), logical coordinates; never filtered. */
136
+ lighting: RuntimeSnapshotLighting;
137
+ /** Post Effects (issue #78 CA-12), each null when off; never filtered. */
138
+ post: RuntimeSnapshotPost;
139
+ /** The live 3D scene's physics world: state, gravity and bodies (issue #159 CA-17); absent in a 2D scene, never filtered. */
140
+ physics?: RuntimeSnapshotPhysics;
127
141
  }
128
142
  export declare const RUNTIME_PROJECTION_LIMITS: {
129
143
  readonly depth: 5;
@@ -1,5 +1,8 @@
1
+ import { cameraEffectsSnapshot, viewSnapshot } from './runtime-view-snapshot.js';
1
2
  import { anchoredPiecesOf } from './ui.js';
2
3
  import { componentClassOf } from './component-registry.js';
4
+ import { capLights, lightingSnapshot, postSnapshot, } from './runtime-lighting-snapshot.js';
5
+ import { capBodies, physicsSection } from './runtime-physics-snapshot.js';
3
6
  export const RUNTIME_PROJECTION_LIMITS = {
4
7
  depth: 5,
5
8
  entries: 100,
@@ -292,7 +295,12 @@ export class RuntimeInspector {
292
295
  audio: this.audioSnapshot(),
293
296
  time: this.timeSnapshot(),
294
297
  ui: this.uiSnapshot(projectionIssues),
295
- camera: this.game.cameraEffects.state,
298
+ camera: cameraEffectsSnapshot(this.game),
299
+ space: this.game.space,
300
+ view: viewSnapshot(this.game),
301
+ lighting: lightingSnapshot(this.game, (entity) => this.idFor(entity)),
302
+ post: postSnapshot(this.game),
303
+ ...physicsSection(this.game, (entity) => this.idFor(entity)),
296
304
  });
297
305
  }
298
306
  audioSnapshot() {
@@ -341,7 +349,9 @@ export class RuntimeInspector {
341
349
  if (fitsSnapshot(capped))
342
350
  return capped;
343
351
  }
344
- return capAnchored(capped);
352
+ const anchoredCapped = capAnchored(capped);
353
+ const bodiesCapped = capBodies(anchoredCapped, fitsSnapshot);
354
+ return fitsSnapshot(bodiesCapped) ? bodiesCapped : capLights(bodiesCapped, fitsSnapshot);
345
355
  }
346
356
  idFor(entity) {
347
357
  const existing = this.ids.get(entity);
@@ -0,0 +1,67 @@
1
+ import type { Entity } from './entity.js';
2
+ import type { Game } from './game.js';
3
+ import type { ProjectionIssue } from './runtime-inspection.js';
4
+ import type { Vec3Json } from './scene-camera-3d.js';
5
+ import type { AmbientLight, PostEffectsState } from './scene-render-options.js';
6
+ /** One live Light as a Runtime Snapshot reports it: its entity's name, logical position and params. */
7
+ export interface RuntimeSnapshotLight {
8
+ /** The entity's name, as `ui.anchored[].entity` reports it. */
9
+ entity: string;
10
+ /** The entity's snapshot id (`entity-N`), unique where names repeat. */
11
+ id: string;
12
+ x: number;
13
+ y: number;
14
+ radius: number;
15
+ /** `#rrggbb`. */
16
+ color: string;
17
+ intensity: number;
18
+ bands: number;
19
+ softness: number;
20
+ castShadows: boolean;
21
+ }
22
+ /** One live Sun in a 3D scene (issue #154 CA-16): its entity's name and id, the unit direction it shines along, color and intensity. */
23
+ export interface RuntimeSnapshotSun {
24
+ entity: string;
25
+ id: string;
26
+ direction: Vec3Json;
27
+ /** `#rrggbb`. */
28
+ color: string;
29
+ intensity: number;
30
+ }
31
+ /** One live Point Light in a 3D scene (issue #154 CA-16): its world position and look; `distance` 0 is unlimited. */
32
+ export interface RuntimeSnapshotPointLight {
33
+ entity: string;
34
+ id: string;
35
+ position: Vec3Json;
36
+ /** `#rrggbb`. */
37
+ color: string;
38
+ intensity: number;
39
+ distance: number;
40
+ }
41
+ /**
42
+ * `game.lighting` (issue #78 CA-12): the Ambient Light now and every live
43
+ * Light, in logical coordinates. A 3D scene (issue #154 CA-16) adds its
44
+ * Suns and Point Lights; a 2D scene's snapshot has neither key.
45
+ */
46
+ export interface RuntimeSnapshotLighting {
47
+ ambient: AmbientLight;
48
+ lights: RuntimeSnapshotLight[];
49
+ sun?: RuntimeSnapshotSun[];
50
+ pointLights?: RuntimeSnapshotPointLight[];
51
+ }
52
+ /** `game.post` (issue #78 CA-12): each Post Effect, null when off. */
53
+ export type RuntimeSnapshotPost = PostEffectsState;
54
+ export declare function lightingSnapshot(game: Game, idFor: (entity: Entity) => string): RuntimeSnapshotLighting;
55
+ export declare function postSnapshot(game: Game): RuntimeSnapshotPost;
56
+ /** What capping the lights needs from a snapshot: its lights and its projection issues. */
57
+ interface LightsCappable {
58
+ lighting: RuntimeSnapshotLighting;
59
+ projectionIssues: ProjectionIssue[];
60
+ }
61
+ /**
62
+ * The global cap's last stage, once entities and Anchored Pieces are gone
63
+ * (review): drops lights from the end until the snapshot fits, recording how
64
+ * many went with a `truncated` marker at `lighting.lights[n]`.
65
+ */
66
+ export declare function capLights<T extends LightsCappable>(snapshot: T, fits: (candidate: T) => boolean): T;
67
+ export {};
@@ -0,0 +1,77 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import { PointLight } from './components/point-light.js';
3
+ import { Sun } from './components/sun.js';
4
+ const hex = (channel) => Math.round(channel * 255).toString(16).padStart(2, '0');
5
+ const rounded = (value) => Math.round(value * 1e6) / 1e6 || 0;
6
+ const hexOf = (color) => `#${(color & 0xffffff).toString(16).padStart(6, '0')}`;
7
+ function unit(direction) {
8
+ const [x, y, z] = new THREE.Vector3(...direction).normalize().toArray();
9
+ return [rounded(x), rounded(y), rounded(z)];
10
+ }
11
+ /** The Suns and Point Lights of a 3D scene's entities, in spawn order. */
12
+ function lights3dSnapshot(game, idFor) {
13
+ const sun = [];
14
+ const pointLights = [];
15
+ for (const entity of game.entities) {
16
+ for (const component of entity.components) {
17
+ if (component instanceof Sun) {
18
+ const { direction, color, intensity } = component;
19
+ sun.push({ entity: entity.name, id: idFor(entity), direction: unit(direction), color: hexOf(color), intensity });
20
+ }
21
+ else if (component instanceof PointLight && component.light) {
22
+ const { color, intensity, distance, light } = component;
23
+ light.updateWorldMatrix(true, false);
24
+ const at = light.getWorldPosition(new THREE.Vector3());
25
+ const position = [rounded(at.x), rounded(at.y), rounded(at.z)];
26
+ pointLights.push({ entity: entity.name, id: idFor(entity), position, color: hexOf(color), intensity, distance });
27
+ }
28
+ }
29
+ }
30
+ return { sun, pointLights };
31
+ }
32
+ export function lightingSnapshot(game, idFor) {
33
+ return {
34
+ ...(game.space === '3d' ? lights3dSnapshot(game, idFor) : {}),
35
+ ambient: game.lighting.ambient,
36
+ lights: game.lighting.lights.map((light) => {
37
+ const field = light.field();
38
+ const [r, g, b] = field.color;
39
+ return {
40
+ entity: light.entity.name,
41
+ id: idFor(light.entity),
42
+ x: field.x,
43
+ y: field.y,
44
+ radius: field.radius,
45
+ color: `#${hex(r)}${hex(g)}${hex(b)}`,
46
+ intensity: field.intensity,
47
+ bands: field.bands,
48
+ softness: field.softness,
49
+ castShadows: field.castShadows,
50
+ };
51
+ }),
52
+ };
53
+ }
54
+ export function postSnapshot(game) {
55
+ return { vignette: game.post.vignette, colorGrade: game.post.colorGrade };
56
+ }
57
+ /**
58
+ * The global cap's last stage, once entities and Anchored Pieces are gone
59
+ * (review): drops lights from the end until the snapshot fits, recording how
60
+ * many went with a `truncated` marker at `lighting.lights[n]`.
61
+ */
62
+ export function capLights(snapshot, fits) {
63
+ let capped = snapshot;
64
+ const retained = [...snapshot.lighting.lights];
65
+ while (retained.length > 0) {
66
+ retained.pop();
67
+ const omitted = snapshot.lighting.lights.length - retained.length;
68
+ capped = {
69
+ ...snapshot,
70
+ lighting: { ...snapshot.lighting, lights: [...retained] },
71
+ projectionIssues: [...snapshot.projectionIssues, { path: `lighting.lights[${retained.length}]`, marker: 'truncated', omitted }],
72
+ };
73
+ if (fits(capped))
74
+ return capped;
75
+ }
76
+ return capped;
77
+ }
@@ -0,0 +1,41 @@
1
+ import type { Entity } from './entity.js';
2
+ import type { Game } from './game.js';
3
+ import { type BodyKind } from './physics-3d/body-sync.js';
4
+ import type { PhysicsState } from './physics-3d/physics-host.js';
5
+ import type { ProjectionIssue } from './runtime-inspection.js';
6
+ import type { Vec3Json } from './scene-camera-3d.js';
7
+ /** One live body of a 3D scene (issue #159 CA-17): its entity's name and snapshot id, its Rapier type, velocity and whether it stands on something. */
8
+ export interface RuntimeSnapshotBody {
9
+ entity: string;
10
+ id: string;
11
+ type: BodyKind;
12
+ velocity: Vec3Json;
13
+ grounded: boolean;
14
+ }
15
+ /**
16
+ * `game.physics` of a 3D scene (issue #159 CA-17), beside `lighting`: the
17
+ * world's state (`loading` until the Rapier module arrives), its gravity and
18
+ * every body in spawn order, rounded to 1e-6. Never filtered by entity or
19
+ * component filters; a 2D scene's snapshot has no `physics` key.
20
+ */
21
+ export interface RuntimeSnapshotPhysics {
22
+ state: PhysicsState;
23
+ gravity: Vec3Json;
24
+ bodies: RuntimeSnapshotBody[];
25
+ }
26
+ /** The `physics` section of a 3D scene's snapshot, as the spread that adds it; nothing in a 2D scene. */
27
+ export declare function physicsSection(game: Game, idFor: (entity: Entity) => string): {
28
+ physics?: RuntimeSnapshotPhysics;
29
+ };
30
+ /** What capping the bodies needs from a snapshot: its physics section and its projection issues. */
31
+ interface BodiesCappable {
32
+ physics?: RuntimeSnapshotPhysics;
33
+ projectionIssues: ProjectionIssue[];
34
+ }
35
+ /**
36
+ * The global cap's stage for bodies, beside the lights' (`capLights`): drops
37
+ * bodies from the end until the snapshot fits, recording how many went with a
38
+ * `truncated` marker at `physics.bodies[n]`.
39
+ */
40
+ export declare function capBodies<T extends BodiesCappable>(snapshot: T, fits: (candidate: T) => boolean): T;
41
+ export {};
@@ -0,0 +1,45 @@
1
+ import { bodyVelocity } from './physics-3d/body-sync.js';
2
+ const rounded = (value) => Math.round(value * 1e6) / 1e6 || 0;
3
+ const roundedVector = (x, y, z) => [rounded(x), rounded(y), rounded(z)];
4
+ /** The `physics` section of a 3D scene's snapshot, as the spread that adds it; nothing in a 2D scene. */
5
+ export function physicsSection(game, idFor) {
6
+ if (game.space !== '3d')
7
+ return {};
8
+ const world = game.physics.world;
9
+ const [gx, gy, gz] = game.physics.sceneGravity;
10
+ const bodies = (world?.bodiesOf(game.entities) ?? []).map((record) => {
11
+ const { x, y, z } = bodyVelocity(record);
12
+ return {
13
+ entity: record.entity.name,
14
+ id: idFor(record.entity),
15
+ type: record.kind,
16
+ velocity: roundedVector(x, y, z),
17
+ grounded: record.grounded,
18
+ };
19
+ });
20
+ return { physics: { state: game.physics.state, gravity: roundedVector(gx, gy, gz), bodies } };
21
+ }
22
+ /**
23
+ * The global cap's stage for bodies, beside the lights' (`capLights`): drops
24
+ * bodies from the end until the snapshot fits, recording how many went with a
25
+ * `truncated` marker at `physics.bodies[n]`.
26
+ */
27
+ export function capBodies(snapshot, fits) {
28
+ const physics = snapshot.physics;
29
+ if (!physics || fits(snapshot))
30
+ return snapshot;
31
+ let capped = snapshot;
32
+ const retained = [...physics.bodies];
33
+ while (retained.length > 0) {
34
+ retained.pop();
35
+ const omitted = physics.bodies.length - retained.length;
36
+ capped = {
37
+ ...snapshot,
38
+ physics: { ...physics, bodies: [...retained] },
39
+ projectionIssues: [...snapshot.projectionIssues, { path: `physics.bodies[${retained.length}]`, marker: 'truncated', omitted }],
40
+ };
41
+ if (fits(capped))
42
+ return capped;
43
+ }
44
+ return capped;
45
+ }
@@ -0,0 +1,25 @@
1
+ import type { CameraEffectsState } from './camera-effects.js';
2
+ import type { Game } from './game.js';
3
+ import type { Vec3Json } from './scene-camera-3d.js';
4
+ /**
5
+ * The view the scene is drawn from (issue #154 CA-16), beside the Camera
6
+ * Effects `camera`: the orthographic camera's centre and visible height, or
7
+ * the perspective camera's pose. Always reflects the live camera.
8
+ */
9
+ export type RuntimeSnapshotView = {
10
+ kind: 'orthographic';
11
+ position: [number, number];
12
+ zoom: number;
13
+ } | {
14
+ kind: 'perspective';
15
+ position: Vec3Json;
16
+ target: Vec3Json;
17
+ fov: number;
18
+ };
19
+ export declare function viewSnapshot(game: Game): RuntimeSnapshotView;
20
+ /**
21
+ * `game.cameraEffects.state` as the snapshot reports it. A perspective camera
22
+ * does not shake (spec decision 24: shake is a no-op in a 3D scene), so a 3D
23
+ * scene reports no offset even though the effect's own state keeps decaying.
24
+ */
25
+ export declare function cameraEffectsSnapshot(game: Game): CameraEffectsState;
@@ -0,0 +1,30 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import { isPerspectiveCamera } from './camera-projection.js';
3
+ /** Float noise off a pose, so a camera that did not move reports the numbers it was declared with. */
4
+ const clean = (value) => Math.round(value * 1e6) / 1e6 || 0;
5
+ function vector(value) {
6
+ return [clean(value.x), clean(value.y), clean(value.z)];
7
+ }
8
+ export function viewSnapshot(game) {
9
+ const camera = game.camera;
10
+ if (!isPerspectiveCamera(camera)) {
11
+ return { kind: 'orthographic', position: [clean(camera.position.x), clean(camera.position.y)], zoom: game.view };
12
+ }
13
+ camera.updateMatrixWorld();
14
+ const ahead = camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(game.cameraRig.lookDistance);
15
+ return {
16
+ kind: 'perspective',
17
+ position: vector(camera.position),
18
+ target: vector(camera.position.clone().add(ahead)),
19
+ fov: camera.fov,
20
+ };
21
+ }
22
+ /**
23
+ * `game.cameraEffects.state` as the snapshot reports it. A perspective camera
24
+ * does not shake (spec decision 24: shake is a no-op in a 3D scene), so a 3D
25
+ * scene reports no offset even though the effect's own state keeps decaying.
26
+ */
27
+ export function cameraEffectsSnapshot(game) {
28
+ const state = game.cameraEffects.state;
29
+ return game.space === '3d' ? { ...state, shake: { x: 0, y: 0 } } : state;
30
+ }