@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,66 @@
1
+ import type * as THREE from 'three/webgpu';
2
+ /** A world-space point or direction as scene JSON stores it. */
3
+ export type Vec3Json = [number, number, number];
4
+ /**
5
+ * A scene's perspective camera (3D scenes, ADR 0027). Fixed by the scene; game
6
+ * code may move it. Y is up and the camera looks from `position` at `target`.
7
+ */
8
+ export interface PerspectiveSceneCameraJson {
9
+ kind: 'perspective';
10
+ /** Where the camera sits. Default `[0, 5, 10]`. */
11
+ position?: Vec3Json;
12
+ /** The point it looks at. Default `[0, 0, 0]`. */
13
+ target?: Vec3Json;
14
+ /** Vertical field of view in degrees, in (0, 180). Default 60. */
15
+ fov?: number;
16
+ /** Near clipping plane, above 0. Default 0.1. */
17
+ near?: number;
18
+ /** Far clipping plane, above `near`. Default 1000. */
19
+ far?: number;
20
+ zoom?: never;
21
+ follow?: never;
22
+ deadzoneWidth?: never;
23
+ deadzoneHeight?: never;
24
+ lookahead?: never;
25
+ lookaheadY?: never;
26
+ smoothing?: never;
27
+ limits?: never;
28
+ }
29
+ export interface ResolvedPerspectiveCamera {
30
+ kind: 'perspective';
31
+ position: Vec3Json;
32
+ target: Vec3Json;
33
+ fov: number;
34
+ near: number;
35
+ far: number;
36
+ }
37
+ export declare const PERSPECTIVE_DEFAULTS: {
38
+ readonly position: Vec3Json;
39
+ readonly target: Vec3Json;
40
+ readonly fov: 60;
41
+ readonly near: 0.1;
42
+ readonly far: 1000;
43
+ };
44
+ /** True for a perspective camera block; everything else (or none) is the orthographic one. */
45
+ export declare function isPerspectiveCameraJson(json: {
46
+ kind?: string;
47
+ } | undefined): json is PerspectiveSceneCameraJson;
48
+ /** Fills a perspective camera block with the engine defaults. */
49
+ export declare function resolvePerspectiveCamera(json?: PerspectiveSceneCameraJson): ResolvedPerspectiveCamera;
50
+ /** Points `camera` as the resolved block says, at the given aspect ratio. */
51
+ export declare function placePerspectiveCamera(camera: THREE.PerspectiveCamera, resolved: ResolvedPerspectiveCamera, aspect: number): void;
52
+ /** One field of a scene's render, camera or entity transform that is invalid, for validate_project. */
53
+ export interface SceneFieldIssue {
54
+ field: string;
55
+ message: string;
56
+ }
57
+ /** A JSON value as a message quotes it: strings in quotes, everything else as `String()` shows it. */
58
+ export declare const shown: (value: unknown) => string;
59
+ export declare function isFiniteNumber(value: unknown): value is number;
60
+ /**
61
+ * Every field of a perspective camera block outside its range: `fov` in
62
+ * (0, 180), `near` above 0, `far` above `near`, and `position` and `target`
63
+ * three finite numbers, and none of the orthographic block's fields. Anything
64
+ * that is not a perspective block reports none.
65
+ */
66
+ export declare function perspectiveCameraIssues(camera: unknown): SceneFieldIssue[];
@@ -0,0 +1,117 @@
1
+ export const PERSPECTIVE_DEFAULTS = {
2
+ position: [0, 5, 10],
3
+ target: [0, 0, 0],
4
+ fov: 60,
5
+ near: 0.1,
6
+ far: 1000,
7
+ };
8
+ /** True for a perspective camera block; everything else (or none) is the orthographic one. */
9
+ export function isPerspectiveCameraJson(json) {
10
+ return json?.kind === 'perspective';
11
+ }
12
+ /** Fills a perspective camera block with the engine defaults. */
13
+ export function resolvePerspectiveCamera(json = { kind: 'perspective' }) {
14
+ return {
15
+ kind: 'perspective',
16
+ position: json.position ?? PERSPECTIVE_DEFAULTS.position,
17
+ target: json.target ?? PERSPECTIVE_DEFAULTS.target,
18
+ fov: json.fov ?? PERSPECTIVE_DEFAULTS.fov,
19
+ near: json.near ?? PERSPECTIVE_DEFAULTS.near,
20
+ far: json.far ?? PERSPECTIVE_DEFAULTS.far,
21
+ };
22
+ }
23
+ /** Points `camera` as the resolved block says, at the given aspect ratio. */
24
+ export function placePerspectiveCamera(camera, resolved, aspect) {
25
+ camera.fov = resolved.fov;
26
+ camera.near = resolved.near;
27
+ camera.far = resolved.far;
28
+ camera.aspect = aspect;
29
+ camera.position.set(...resolved.position);
30
+ camera.lookAt(...resolved.target);
31
+ camera.updateProjectionMatrix();
32
+ camera.updateMatrixWorld();
33
+ }
34
+ /** A JSON value as a message quotes it: strings in quotes, everything else as `String()` shows it. */
35
+ export const shown = (value) => (typeof value === 'string' ? JSON.stringify(value) : String(value));
36
+ export function isFiniteNumber(value) {
37
+ return typeof value === 'number' && Number.isFinite(value);
38
+ }
39
+ function vectorIssue(field, value, issues) {
40
+ if (value === undefined)
41
+ return;
42
+ if (Array.isArray(value) && value.length === 3 && value.every(isFiniteNumber))
43
+ return;
44
+ issues.push({ field, message: `${field} must be three finite numbers [x, y, z]; got ${JSON.stringify(value)}.` });
45
+ }
46
+ function fovIssue(value, issues) {
47
+ if (value === undefined)
48
+ return;
49
+ if (isFiniteNumber(value) && value > 0 && value < 180)
50
+ return;
51
+ issues.push({ field: 'camera.fov', message: `camera.fov must be a number above 0 and below 180; got ${shown(value)}.` });
52
+ }
53
+ /** `position` and `target` as the camera would use them, when both are well-formed (declared or default). */
54
+ function resolvedAim(block) {
55
+ const position = block.position ?? PERSPECTIVE_DEFAULTS.position;
56
+ const target = block.target ?? PERSPECTIVE_DEFAULTS.target;
57
+ const wellFormed = (value) => Array.isArray(value) && value.length === 3 && value.every(isFiniteNumber);
58
+ return wellFormed(position) && wellFormed(target) ? { position, target } : null;
59
+ }
60
+ function aimIssue(block, issues) {
61
+ const aim = resolvedAim(block);
62
+ if (!aim || aim.position.some((value, axis) => value !== aim.target[axis]))
63
+ return;
64
+ issues.push({
65
+ field: 'camera.target',
66
+ message: `camera.target must differ from camera.position (${JSON.stringify(aim.position)}): a camera needs a direction to look in.`,
67
+ });
68
+ }
69
+ function planeIssues(near, far, issues) {
70
+ const nearIsValid = isFiniteNumber(near) && near > 0;
71
+ if (near !== undefined && !nearIsValid) {
72
+ issues.push({ field: 'camera.near', message: `camera.near must be a number above 0; got ${shown(near)}.` });
73
+ }
74
+ const nearValue = nearIsValid ? near : PERSPECTIVE_DEFAULTS.near;
75
+ if (far !== undefined && !(isFiniteNumber(far) && far > nearValue)) {
76
+ issues.push({ field: 'camera.far', message: `camera.far must be a number above near (${nearValue}); got ${shown(far)}.` });
77
+ }
78
+ }
79
+ /** The orthographic block's fields, which a perspective block does not accept. */
80
+ const ORTHOGRAPHIC_ONLY_FIELDS = [
81
+ 'follow',
82
+ 'zoom',
83
+ 'deadzoneWidth',
84
+ 'deadzoneHeight',
85
+ 'lookahead',
86
+ 'lookaheadY',
87
+ 'smoothing',
88
+ 'limits',
89
+ ];
90
+ function orthographicFieldIssues(block, issues) {
91
+ for (const field of ORTHOGRAPHIC_ONLY_FIELDS) {
92
+ if (block[field] === undefined)
93
+ continue;
94
+ issues.push({ field: `camera.${field}`, message: `camera.${field} belongs to an orthographic camera; a perspective camera does not accept it.` });
95
+ }
96
+ }
97
+ /**
98
+ * Every field of a perspective camera block outside its range: `fov` in
99
+ * (0, 180), `near` above 0, `far` above `near`, and `position` and `target`
100
+ * three finite numbers, and none of the orthographic block's fields. Anything
101
+ * that is not a perspective block reports none.
102
+ */
103
+ export function perspectiveCameraIssues(camera) {
104
+ if (typeof camera !== 'object' || camera === null)
105
+ return [];
106
+ const block = { ...camera };
107
+ if (block.kind !== 'perspective')
108
+ return [];
109
+ const issues = [];
110
+ vectorIssue('camera.position', block.position, issues);
111
+ vectorIssue('camera.target', block.target, issues);
112
+ fovIssue(block.fov, issues);
113
+ aimIssue(block, issues);
114
+ planeIssues(block.near, block.far, issues);
115
+ orthographicFieldIssues(block, issues);
116
+ return issues;
117
+ }
@@ -0,0 +1,52 @@
1
+ import type { Light } from './components/light.js';
2
+ import type { Rgb } from './light-field.js';
3
+ import { type AmbientLight } from './scene-render-options.js';
4
+ import type { SceneRenderJson } from './scene.js';
5
+ /** What `game.lighting.ambient` accepts: either field, a color as `#rrggbb` or a number. */
6
+ export interface AmbientLightInput {
7
+ color?: string | number;
8
+ intensity?: number;
9
+ }
10
+ /** A `#rrggbb` color's three channels in 0..1. */
11
+ export declare function hexChannels(hex: string): Rgb;
12
+ /**
13
+ * `game.lighting` (issue #78, ADR 0026): the live scene's Ambient Light and
14
+ * its Lights. A scene is drawn lit while it declares `render.lighting`, a
15
+ * game sets the ambient at runtime, or a Light lives in it; otherwise it is
16
+ * drawn exactly as an unlit scene. Everything here dies with its scene.
17
+ */
18
+ export declare class GameLighting {
19
+ constructor();
20
+ /** The Ambient Light now, a copy: color `#rrggbb`, intensity 0..1. */
21
+ get ambient(): AmbientLight;
22
+ /**
23
+ * Changes the Ambient Light, field by field — tween it with `game.time.tween`
24
+ * for a day/night cycle. Out-of-range values clamp; an unreadable color is
25
+ * ignored. Setting it lights a scene that declared no lighting.
26
+ */
27
+ set ambient(value: AmbientLightInput);
28
+ /** Whether the live scene is drawn lit. */
29
+ get active(): boolean;
30
+ /** Every live Light, in the order they were spawned. */
31
+ get lights(): readonly Light[];
32
+ /** Adopts a scene's `render.lighting`, replacing whatever the previous scene left. Called by Game. */
33
+ loadScene(render: SceneRenderJson | undefined): void;
34
+ /** Back to an unlit scene's state: full white, undeclared. Its Lights leave with their entities. */
35
+ unloadScene(): void;
36
+ }
37
+ /** The Ambient Light as light-map multipliers: its color's channels × its intensity. */
38
+ export declare function ambientMultiplier(lighting: GameLighting): Rgb;
39
+ /**
40
+ * Whether the Ambient Light leaves the art as drawn: lights only add and the
41
+ * light-map clamps at 1, so such a scene's light-map is 1 everywhere and the
42
+ * frame is drawn unlit, building nothing (review).
43
+ */
44
+ export declare function ambientIsFull(lighting: GameLighting): boolean;
45
+ /** Internal: a Light joins its scene's lighting when it is ready. */
46
+ export declare function addLight(lighting: GameLighting, light: Light): void;
47
+ /** Internal: a destroyed Light leaves at once, so the next drawn frame no longer has it. */
48
+ export declare function removeLight(lighting: GameLighting, light: Light): void;
49
+ /** Internal: a Tilemap's solid tiles changed (spawned, edited or destroyed); occlusion is rebuilt once. */
50
+ export declare function markOccludersChanged(lighting: GameLighting | undefined): void;
51
+ /** Internal: bumps whenever the solid tiles change, so the occluder grid is rebuilt only then (inference 9). */
52
+ export declare function occluderRevision(lighting: GameLighting): number;
@@ -0,0 +1,107 @@
1
+ import { hexColorOr, resolveAmbientLight } from './scene-render-options.js';
2
+ const states = new WeakMap();
3
+ function stateOf(lighting) {
4
+ const state = states.get(lighting);
5
+ if (!state)
6
+ throw new Error('a GameLighting was used before it was constructed');
7
+ return state;
8
+ }
9
+ /** `0xrrggbb` as `#rrggbb`; anything that is not a 24-bit integer as undefined. */
10
+ function hexOfNumber(value) {
11
+ if (!Number.isInteger(value) || value < 0 || value > 0xffffff)
12
+ return undefined;
13
+ return `#${value.toString(16).padStart(6, '0')}`;
14
+ }
15
+ /** A `#rrggbb` color's three channels in 0..1. */
16
+ export function hexChannels(hex) {
17
+ const value = Number.parseInt(hex.slice(1), 16);
18
+ return [((value >> 16) & 0xff) / 255, ((value >> 8) & 0xff) / 255, (value & 0xff) / 255];
19
+ }
20
+ /**
21
+ * `game.lighting` (issue #78, ADR 0026): the live scene's Ambient Light and
22
+ * its Lights. A scene is drawn lit while it declares `render.lighting`, a
23
+ * game sets the ambient at runtime, or a Light lives in it; otherwise it is
24
+ * drawn exactly as an unlit scene. Everything here dies with its scene.
25
+ */
26
+ export class GameLighting {
27
+ constructor() {
28
+ states.set(this, { ambient: resolveAmbientLight(undefined), declared: false, lights: [], occluderRevision: 0 });
29
+ }
30
+ /** The Ambient Light now, a copy: color `#rrggbb`, intensity 0..1. */
31
+ get ambient() {
32
+ const { ambient } = stateOf(this);
33
+ return { color: ambient.color, intensity: ambient.intensity };
34
+ }
35
+ /**
36
+ * Changes the Ambient Light, field by field — tween it with `game.time.tween`
37
+ * for a day/night cycle. Out-of-range values clamp; an unreadable color is
38
+ * ignored. Setting it lights a scene that declared no lighting.
39
+ */
40
+ set ambient(value) {
41
+ const state = stateOf(this);
42
+ const color = typeof value.color === 'number' ? hexOfNumber(value.color) : value.color;
43
+ // An unreadable intensity (a NaN from a custom easing, say) is ignored, like an unreadable color.
44
+ const intensity = typeof value.intensity === 'number' && Number.isFinite(value.intensity) ? value.intensity : state.ambient.intensity;
45
+ state.ambient = resolveAmbientLight({
46
+ ambient: { color: hexColorOr(color, state.ambient.color), intensity },
47
+ });
48
+ state.declared = true;
49
+ }
50
+ /** Whether the live scene is drawn lit. */
51
+ get active() {
52
+ const state = stateOf(this);
53
+ return state.declared || state.lights.length > 0;
54
+ }
55
+ /** Every live Light, in the order they were spawned. */
56
+ get lights() {
57
+ return [...stateOf(this).lights];
58
+ }
59
+ /** Adopts a scene's `render.lighting`, replacing whatever the previous scene left. Called by Game. */
60
+ loadScene(render) {
61
+ const state = stateOf(this);
62
+ state.ambient = resolveAmbientLight(render?.lighting);
63
+ state.declared = render?.lighting !== undefined;
64
+ }
65
+ /** Back to an unlit scene's state: full white, undeclared. Its Lights leave with their entities. */
66
+ unloadScene() {
67
+ this.loadScene(undefined);
68
+ }
69
+ }
70
+ /** The Ambient Light as light-map multipliers: its color's channels × its intensity. */
71
+ export function ambientMultiplier(lighting) {
72
+ const { color, intensity } = stateOf(lighting).ambient;
73
+ const [r, g, b] = hexChannels(color);
74
+ return [r * intensity, g * intensity, b * intensity];
75
+ }
76
+ /**
77
+ * Whether the Ambient Light leaves the art as drawn: lights only add and the
78
+ * light-map clamps at 1, so such a scene's light-map is 1 everywhere and the
79
+ * frame is drawn unlit, building nothing (review).
80
+ */
81
+ export function ambientIsFull(lighting) {
82
+ return ambientMultiplier(lighting).every((channel) => channel >= 1);
83
+ }
84
+ /** Internal: a Light joins its scene's lighting when it is ready. */
85
+ export function addLight(lighting, light) {
86
+ const { lights } = stateOf(lighting);
87
+ if (!lights.includes(light))
88
+ lights.push(light);
89
+ }
90
+ /** Internal: a destroyed Light leaves at once, so the next drawn frame no longer has it. */
91
+ export function removeLight(lighting, light) {
92
+ const { lights } = stateOf(lighting);
93
+ const index = lights.indexOf(light);
94
+ if (index !== -1)
95
+ lights.splice(index, 1);
96
+ }
97
+ /** Internal: a Tilemap's solid tiles changed (spawned, edited or destroyed); occlusion is rebuilt once. */
98
+ export function markOccludersChanged(lighting) {
99
+ // A Tilemap on a stand-in game (a test stub with no lighting) has nothing to notify.
100
+ const state = lighting ? states.get(lighting) : undefined;
101
+ if (state)
102
+ state.occluderRevision += 1;
103
+ }
104
+ /** Internal: bumps whenever the solid tiles change, so the occluder grid is rebuilt only then (inference 9). */
105
+ export function occluderRevision(lighting) {
106
+ return stateOf(lighting).occluderRevision;
107
+ }
@@ -0,0 +1,17 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import type { SceneSpace } from './scene-space.js';
3
+ import type { AmbientLight } from './scene-render-options.js';
4
+ /**
5
+ * The Ambient Light of a 3D scene (ADR 0027): one `THREE.AmbientLight` the
6
+ * Game owns, fed by `game.lighting.ambient` (`render.lighting.ambient` and the
7
+ * runtime setter), color × intensity, full white when nothing says otherwise.
8
+ * It exists only while the live scene is 3D; a 2D scene lights through the
9
+ * light-map instead (ADR 0026).
10
+ */
11
+ export declare class SceneAmbientLight3d {
12
+ private readonly scene;
13
+ private light;
14
+ constructor(scene: THREE.Scene);
15
+ /** Adds, updates or removes the light for the live scene's space; called once per frame before it is drawn. */
16
+ sync(space: SceneSpace, ambient: AmbientLight): void;
17
+ }
@@ -0,0 +1,27 @@
1
+ import * as THREE from 'three/webgpu';
2
+ /**
3
+ * The Ambient Light of a 3D scene (ADR 0027): one `THREE.AmbientLight` the
4
+ * Game owns, fed by `game.lighting.ambient` (`render.lighting.ambient` and the
5
+ * runtime setter), color × intensity, full white when nothing says otherwise.
6
+ * It exists only while the live scene is 3D; a 2D scene lights through the
7
+ * light-map instead (ADR 0026).
8
+ */
9
+ export class SceneAmbientLight3d {
10
+ scene;
11
+ light = null;
12
+ constructor(scene) {
13
+ this.scene = scene;
14
+ }
15
+ /** Adds, updates or removes the light for the live scene's space; called once per frame before it is drawn. */
16
+ sync(space, ambient) {
17
+ if (space !== '3d') {
18
+ this.light?.removeFromParent();
19
+ return;
20
+ }
21
+ this.light ??= new THREE.AmbientLight();
22
+ if (this.light.parent !== this.scene)
23
+ this.scene.add(this.light);
24
+ this.light.color.set(ambient.color);
25
+ this.light.intensity = ambient.intensity;
26
+ }
27
+ }
@@ -0,0 +1,51 @@
1
+ import type { SceneLightingJson, ScenePostJson } from './scene.js';
2
+ /** The Ambient Light: a `#rrggbb` color and an intensity in 0..1. */
3
+ export interface AmbientLight {
4
+ color: string;
5
+ intensity: number;
6
+ }
7
+ export interface VignetteEffect {
8
+ /** How dark the corners get: 0 none, 1 black. */
9
+ intensity: number;
10
+ /** Where the darkening starts, from the centre (0) to the corners (1). */
11
+ radius: number;
12
+ }
13
+ export interface ColorGradeEffect {
14
+ /** A `#rrggbb` multiplier over the frame. */
15
+ tint: string;
16
+ /** 1 leaves contrast unchanged; 0 flattens to grey, 2 doubles it. */
17
+ contrast: number;
18
+ /** 1 leaves saturation unchanged; 0 is greyscale, 2 doubles it. */
19
+ saturation: number;
20
+ }
21
+ /** The scene's Post Effects; null is an effect that is off. */
22
+ export interface PostEffectsState {
23
+ vignette: VignetteEffect | null;
24
+ colorGrade: ColorGradeEffect | null;
25
+ }
26
+ /** One field of a scene's `render` block that is outside its range, for validate_project. */
27
+ export interface SceneRenderIssue {
28
+ field: string;
29
+ message: string;
30
+ }
31
+ /** A `#rrggbb` color in lower case, or `fallback` for anything else. */
32
+ export declare function hexColorOr(value: unknown, fallback: string): string;
33
+ /**
34
+ * The scene's starting Ambient Light: full white light unless `render.lighting.ambient`
35
+ * says otherwise. Runtime clamping (inference 11): an out-of-range value is
36
+ * clamped and an unreadable one falls back to its default, never thrown.
37
+ */
38
+ export declare function resolveAmbientLight(json: SceneLightingJson | undefined): AmbientLight;
39
+ /** A vignette as the runtime applies it, clamped; null when the scene has none. */
40
+ export declare function resolveVignette(value: unknown): VignetteEffect | null;
41
+ /** A color grade as the runtime applies it, absent fields neutral; null when the scene has none. */
42
+ export declare function resolveColorGrade(value: unknown): ColorGradeEffect | null;
43
+ /** The scene's starting Post Effects: every one off unless `render.post` turns it on. */
44
+ export declare function resolvePostEffects(json: ScenePostJson | undefined): PostEffectsState;
45
+ /**
46
+ * Every field of a scene's `render.lighting` and `render.post` that is
47
+ * outside its range, each with a message naming the field and its range —
48
+ * what the MCP scene validation reports (CA-2). Other render fields are not
49
+ * checked here.
50
+ */
51
+ export declare function sceneRenderIssues(render: unknown): SceneRenderIssue[];
@@ -0,0 +1,126 @@
1
+ const WHITE = '#ffffff';
2
+ const HEX_COLOR = /^#[0-9a-f]{6}$/i;
3
+ /** Each numeric field's range (inference 12). */
4
+ const RANGES = {
5
+ 'render.lighting.ambient.intensity': [0, 1],
6
+ 'render.post.vignette.intensity': [0, 1],
7
+ 'render.post.vignette.radius': [0, 1],
8
+ 'render.post.colorGrade.contrast': [0, 2],
9
+ 'render.post.colorGrade.saturation': [0, 2],
10
+ };
11
+ function isRecord(value) {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+ function recordOf(value) {
15
+ return isRecord(value) ? value : {};
16
+ }
17
+ /** A `#rrggbb` color in lower case, or `fallback` for anything else. */
18
+ export function hexColorOr(value, fallback) {
19
+ return typeof value === 'string' && HEX_COLOR.test(value) ? value.toLowerCase() : fallback;
20
+ }
21
+ /** A finite number clamped to the field's range, or `fallback` for anything else. */
22
+ function rangedOr(value, field, fallback) {
23
+ if (typeof value !== 'number' || !Number.isFinite(value))
24
+ return fallback;
25
+ const [min, max] = RANGES[field];
26
+ return Math.min(max, Math.max(min, value));
27
+ }
28
+ /**
29
+ * The scene's starting Ambient Light: full white light unless `render.lighting.ambient`
30
+ * says otherwise. Runtime clamping (inference 11): an out-of-range value is
31
+ * clamped and an unreadable one falls back to its default, never thrown.
32
+ */
33
+ export function resolveAmbientLight(json) {
34
+ const ambient = recordOf(recordOf(json).ambient);
35
+ return {
36
+ color: hexColorOr(ambient.color, WHITE),
37
+ intensity: rangedOr(ambient.intensity, 'render.lighting.ambient.intensity', 1),
38
+ };
39
+ }
40
+ /** A vignette as the runtime applies it, clamped; null when the scene has none. */
41
+ export function resolveVignette(value) {
42
+ if (!isRecord(value))
43
+ return null;
44
+ return {
45
+ intensity: rangedOr(value.intensity, 'render.post.vignette.intensity', 0.5),
46
+ radius: rangedOr(value.radius, 'render.post.vignette.radius', 0.5),
47
+ };
48
+ }
49
+ /** A color grade as the runtime applies it, absent fields neutral; null when the scene has none. */
50
+ export function resolveColorGrade(value) {
51
+ if (!isRecord(value))
52
+ return null;
53
+ return {
54
+ tint: hexColorOr(value.tint, WHITE),
55
+ contrast: rangedOr(value.contrast, 'render.post.colorGrade.contrast', 1),
56
+ saturation: rangedOr(value.saturation, 'render.post.colorGrade.saturation', 1),
57
+ };
58
+ }
59
+ /** The scene's starting Post Effects: every one off unless `render.post` turns it on. */
60
+ export function resolvePostEffects(json) {
61
+ const post = recordOf(json);
62
+ return { vignette: resolveVignette(post.vignette), colorGrade: resolveColorGrade(post.colorGrade) };
63
+ }
64
+ const shown = (value) => (typeof value === 'string' ? JSON.stringify(value) : String(value));
65
+ function rangeIssue(field, value, issues) {
66
+ const [min, max] = RANGES[field];
67
+ if (typeof value === 'number' && Number.isFinite(value) && value >= min && value <= max)
68
+ return;
69
+ issues.push({ field, message: `${field} must be a number from ${min} to ${max}; got ${shown(value)}.` });
70
+ }
71
+ function colorIssue(field, value, issues) {
72
+ if (value === undefined || (typeof value === 'string' && HEX_COLOR.test(value)))
73
+ return;
74
+ issues.push({ field, message: `${field} must be a #rrggbb color; got ${shown(value)}.` });
75
+ }
76
+ /** The block at `field` (its last segment, in `parent`), reported when present but not an object. */
77
+ function block(parent, field, issues) {
78
+ const value = parent[field.slice(field.lastIndexOf('.') + 1)];
79
+ if (value === undefined)
80
+ return undefined;
81
+ if (isRecord(value))
82
+ return value;
83
+ issues.push({ field, message: `${field} must be an object; got ${shown(value)}.` });
84
+ return undefined;
85
+ }
86
+ function lightingIssues(render, issues) {
87
+ const lighting = block(render, 'render.lighting', issues);
88
+ const ambient = lighting && block(lighting, 'render.lighting.ambient', issues);
89
+ if (!ambient)
90
+ return;
91
+ colorIssue('render.lighting.ambient.color', ambient.color, issues);
92
+ if (ambient.intensity !== undefined)
93
+ rangeIssue('render.lighting.ambient.intensity', ambient.intensity, issues);
94
+ }
95
+ function postIssues(render, issues) {
96
+ const post = block(render, 'render.post', issues);
97
+ if (!post)
98
+ return;
99
+ const vignette = block(post, 'render.post.vignette', issues);
100
+ if (vignette) {
101
+ rangeIssue('render.post.vignette.intensity', vignette.intensity, issues);
102
+ rangeIssue('render.post.vignette.radius', vignette.radius, issues);
103
+ }
104
+ const grade = block(post, 'render.post.colorGrade', issues);
105
+ if (!grade)
106
+ return;
107
+ colorIssue('render.post.colorGrade.tint', grade.tint, issues);
108
+ if (grade.contrast !== undefined)
109
+ rangeIssue('render.post.colorGrade.contrast', grade.contrast, issues);
110
+ if (grade.saturation !== undefined)
111
+ rangeIssue('render.post.colorGrade.saturation', grade.saturation, issues);
112
+ }
113
+ /**
114
+ * Every field of a scene's `render.lighting` and `render.post` that is
115
+ * outside its range, each with a message naming the field and its range —
116
+ * what the MCP scene validation reports (CA-2). Other render fields are not
117
+ * checked here.
118
+ */
119
+ export function sceneRenderIssues(render) {
120
+ if (!isRecord(render))
121
+ return [];
122
+ const issues = [];
123
+ lightingIssues(render, issues);
124
+ postIssues(render, issues);
125
+ return issues;
126
+ }
@@ -0,0 +1,22 @@
1
+ import { type SceneFieldIssue, type Vec3Json } from './scene-camera-3d.js';
2
+ import type { SceneSpace } from './scene-space.js';
3
+ /**
4
+ * A 3D scene's `simulation` block (issue #159): the physics world's settings.
5
+ * Absent, or with `gravity` absent, the world pulls along -y at 9.81.
6
+ */
7
+ export interface SceneSimulationJson {
8
+ /** The world's gravity vector in units per second squared. Default `[0, -9.81, 0]`. */
9
+ gravity?: Vec3Json;
10
+ }
11
+ export interface ResolvedSceneSimulation {
12
+ gravity: Vec3Json;
13
+ }
14
+ export declare const DEFAULT_GRAVITY: Readonly<Vec3Json>;
15
+ /** The simulation a scene runs with: its `simulation` block, defaults filling the rest. A malformed gravity reads as the default (`validate_project` reports it). */
16
+ export declare function resolveSceneSimulation(json: SceneSimulationJson | undefined): ResolvedSceneSimulation;
17
+ /**
18
+ * Every invalid field of a scene's `simulation` block: a gravity that is not
19
+ * three finite numbers in a 3d scene, and any block at all in a 2d one, where
20
+ * nothing simulates.
21
+ */
22
+ export declare function sceneSimulationIssues(simulation: unknown, space: SceneSpace): SceneFieldIssue[];
@@ -0,0 +1,29 @@
1
+ import { isFiniteNumber } from './scene-camera-3d.js';
2
+ export const DEFAULT_GRAVITY = [0, -9.81, 0];
3
+ function isVec3(value) {
4
+ return Array.isArray(value) && value.length === 3 && value.every(isFiniteNumber);
5
+ }
6
+ /** The simulation a scene runs with: its `simulation` block, defaults filling the rest. A malformed gravity reads as the default (`validate_project` reports it). */
7
+ export function resolveSceneSimulation(json) {
8
+ const gravity = json?.gravity;
9
+ return { gravity: isVec3(gravity) ? [...gravity] : [...DEFAULT_GRAVITY] };
10
+ }
11
+ /**
12
+ * Every invalid field of a scene's `simulation` block: a gravity that is not
13
+ * three finite numbers in a 3d scene, and any block at all in a 2d one, where
14
+ * nothing simulates.
15
+ */
16
+ export function sceneSimulationIssues(simulation, space) {
17
+ if (simulation === undefined)
18
+ return [];
19
+ if (space === '2d') {
20
+ return [{ field: 'simulation', message: `A simulation block needs render.space '3d'; this scene is 2d, where nothing simulates.` }];
21
+ }
22
+ if (typeof simulation !== 'object' || simulation === null || Array.isArray(simulation)) {
23
+ return [{ field: 'simulation', message: `simulation must be an object; got ${JSON.stringify(simulation)}.` }];
24
+ }
25
+ const gravity = Reflect.get(simulation, 'gravity');
26
+ if (gravity === undefined || isVec3(gravity))
27
+ return [];
28
+ return [{ field: 'simulation.gravity', message: `simulation.gravity must be three finite numbers [x, y, z]; got ${JSON.stringify(gravity)}.` }];
29
+ }
@@ -0,0 +1,50 @@
1
+ import type { ComponentClass, ComponentSpace } from './component.js';
2
+ import { type SceneFieldIssue } from './scene-camera-3d.js';
3
+ /**
4
+ * A scene's space (ADR 0027): `'2d'` is the engine's orthographic world,
5
+ * where `z` is draw order; `'3d'` makes `z` a world axis under a perspective
6
+ * camera and a real depth buffer. A scene declares it as `render.space`.
7
+ */
8
+ export type SceneSpace = '2d' | '3d';
9
+ export declare const SCENE_SPACES: readonly SceneSpace[];
10
+ /** The scene's space: 3d only when `render.space` says exactly so. */
11
+ export declare function resolveSceneSpace(render: unknown): SceneSpace;
12
+ /**
13
+ * What a scene's render block makes of the draw-order options at runtime. A
14
+ * 3D scene has no y-sort and no isometric projection whatever the block
15
+ * says (`validate_project` reports them); a 2D scene reads them as always.
16
+ */
17
+ export declare function resolveRenderPolicy(render: unknown): {
18
+ space: SceneSpace;
19
+ sort: 'y' | null;
20
+ projection: 'isometric' | null;
21
+ };
22
+ /**
23
+ * What a component class says about the space it belongs to: its `static
24
+ * space` marker, or nothing. Only that slice of `ComponentClass`, so the
25
+ * abstract `Component` and a class the caller builds by hand qualify too.
26
+ */
27
+ export type SpaceMarked = Pick<ComponentClass, 'space'>;
28
+ /** The space a component class belongs to; a class that declares none is `'both'`. */
29
+ export declare function componentSpaceOf(Class: SpaceMarked): ComponentSpace;
30
+ /**
31
+ * True when a component of this class does not belong in a scene of this
32
+ * space: a `'2d'` component in 3d, a `'3d'` one in 2d. Reads the class's
33
+ * `static space` marker, so engine, behavior and project-owned components
34
+ * answer the same way. A class the caller does not know (undefined) is
35
+ * space-neutral.
36
+ */
37
+ export declare function componentSpaceMismatch(space: SceneSpace, Class: SpaceMarked | undefined): boolean;
38
+ /**
39
+ * Every invalid field of a scene's `render.space`, its camera block and the
40
+ * render options a 3d scene cannot use: a space outside `'2d' | '3d'`, a
41
+ * camera that does not match the space, a perspective camera out of range
42
+ * and `render.sort`, `render.projection` or `render.batch` under 3d. Other
43
+ * render fields are `sceneRenderIssues`'s.
44
+ */
45
+ export declare function sceneSpaceIssues(render: unknown, camera: unknown): SceneFieldIssue[];
46
+ /**
47
+ * An entity's `position` (2 or 3 finite numbers), `rotation` (degrees) and
48
+ * `scale` (3 finite numbers each) that are anything else.
49
+ */
50
+ export declare function entityTransformIssues(entity: unknown): SceneFieldIssue[];