@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
@@ -5,6 +5,7 @@ import { locateFrame, sheetCell } from '../animation/sheet.js';
5
5
  import { spritePlacement } from '../sprite-placement.js';
6
6
  import { reportRejection } from '../report-rejection.js';
7
7
  import { spriteBatchesOf, spriteInstanceOf } from '../sprite-batches.js';
8
+ import { setEmissive, TRANSPARENT_TEXEL_ALPHA } from '../render-layers.js';
8
9
  /** Where a frame on a failed sheet draws: the flat (white) untextured quad. */
9
10
  const UNTEXTURED = { texture: null, pixelArt: false, shape: 'rectangle' };
10
11
  const clampAnchor = (value) => Math.min(1, Math.max(0, value));
@@ -20,6 +21,7 @@ const clampAnchor = (value) => Math.min(1, Math.max(0, value));
20
21
  */
21
22
  export class AnimatedSprite extends Component {
22
23
  static componentName = 'AnimatedSprite';
24
+ static space = '2d';
23
25
  static updateAfter = ['StateMachine'];
24
26
  static params = {
25
27
  offsetX: { label: 'x offset' },
@@ -27,6 +29,7 @@ export class AnimatedSprite extends Component {
27
29
  anchorX: { label: 'x anchor', min: 0, max: 1, step: 0.25 },
28
30
  anchorY: { label: 'y anchor', min: 0, max: 1, step: 0.25 },
29
31
  layer: { label: 'layer', min: -5, max: 5, step: 1 },
32
+ emissive: { label: 'emissive' },
30
33
  };
31
34
  static transient = [
32
35
  'current',
@@ -133,6 +136,17 @@ export class AnimatedSprite extends Component {
133
136
  this.flipX = value;
134
137
  this.syncQuad();
135
138
  }
139
+ // Emissive (issue #78 CA-10): never darkened by the light-map, drawn after
140
+ // it at full brightness. On its own render layer.
141
+ _emissive = false;
142
+ get emissive() {
143
+ return this._emissive;
144
+ }
145
+ set emissive(value) {
146
+ this._emissive = value === true;
147
+ if (this.mesh)
148
+ setEmissive(this.mesh, this._emissive);
149
+ }
136
150
  clips = {};
137
151
  initialClip;
138
152
  /** Clip currently playing. */
@@ -182,10 +196,11 @@ export class AnimatedSprite extends Component {
182
196
  this.mesh = instance.anchor;
183
197
  }
184
198
  else {
185
- const material = new THREE.MeshBasicMaterial({ map: this.texs[0], transparent: true });
199
+ const material = new THREE.MeshBasicMaterial({ map: this.texs[0], transparent: true, alphaTest: TRANSPARENT_TEXEL_ALPHA });
186
200
  this.mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
187
201
  }
188
202
  this.mesh.position.z = this.layer * 0.01;
203
+ setEmissive(this.mesh, this._emissive);
189
204
  this.syncQuad();
190
205
  this.entity.node.add(this.mesh);
191
206
  if (this.initialClip)
@@ -0,0 +1,89 @@
1
+ import { Component, type ComponentSpace } from '../component.js';
2
+ import type { Vec3Json } from '../scene-camera-3d.js';
3
+ export type ColliderShape = 'box' | 'sphere' | 'capsule';
4
+ export declare const COLLIDER_SHAPES: readonly ColliderShape[];
5
+ /**
6
+ * The 3D scene's collision shape (ADR 0028), Rapier's model: alone on an
7
+ * entity it is a fixed wall or floor, created at the entity's position and
8
+ * rotation scaled by `entity.scale`, and moving the entity later does not
9
+ * move it. With a `RigidBody` beside it, it is the body that falls, bounces
10
+ * or walks. `sensor: true` makes it a trigger instead — it detects and never
11
+ * pushes — with the 2D Hitbox's `layer` and `collidesWith` semantics (ADR
12
+ * 0016); with the default mask (`['*']`) a sensor is not told about fixed
13
+ * solid colliders (the level's walls and floors), as a Hitbox never sees a
14
+ * Solid — a mask that names their layer still is. `size` is a box's full
15
+ * extents; `radius` a sphere's or capsule's; `height` a capsule's total
16
+ * end-to-end height, at least twice its radius; `offset` is in the entity's
17
+ * local frame. A Collider in a 2D scene creates nothing; one whose params
18
+ * Rapier cannot take creates no body and warns once (`validate_project`
19
+ * reports them as `invalid-collider-param`).
20
+ */
21
+ export declare class Collider extends Component {
22
+ static componentName: string;
23
+ static space: ComponentSpace;
24
+ static params: {
25
+ shape: {
26
+ label: string;
27
+ options: ColliderShape[];
28
+ };
29
+ size: {
30
+ label: string;
31
+ kind: "vector3";
32
+ };
33
+ radius: {
34
+ label: string;
35
+ min: number;
36
+ step: number;
37
+ };
38
+ height: {
39
+ label: string;
40
+ min: number;
41
+ step: number;
42
+ };
43
+ offset: {
44
+ label: string;
45
+ kind: "vector3";
46
+ };
47
+ sensor: {
48
+ label: string;
49
+ };
50
+ layer: {
51
+ label: string;
52
+ };
53
+ collidesWith: {
54
+ label: string;
55
+ kind: "string-list";
56
+ };
57
+ friction: {
58
+ label: string;
59
+ min: number;
60
+ max: number;
61
+ step: number;
62
+ };
63
+ restitution: {
64
+ label: string;
65
+ min: number;
66
+ max: number;
67
+ step: number;
68
+ };
69
+ };
70
+ shape: ColliderShape;
71
+ /** A box's full extents `[x, y, z]`. */
72
+ size: Vec3Json;
73
+ /** A sphere's or capsule's radius. */
74
+ radius: number;
75
+ /** A capsule's total end-to-end height. */
76
+ height: number;
77
+ /** Where the shape sits in the entity's local frame. */
78
+ offset: Vec3Json;
79
+ /** A trigger: detects overlaps, never pushes or is pushed. */
80
+ sensor: boolean;
81
+ /** One exact, case-sensitive `^[a-z][a-z0-9-]*$` membership name. */
82
+ layer: string;
83
+ /** Outgoing layer interest for `onCollide`; `'*'` means every valid layer and `[]` means none. */
84
+ collidesWith: string[];
85
+ friction: number;
86
+ restitution: number;
87
+ onReady(): void;
88
+ onDestroy(): void;
89
+ }
@@ -0,0 +1,57 @@
1
+ import { Component } from '../component.js';
2
+ export const COLLIDER_SHAPES = ['box', 'sphere', 'capsule'];
3
+ /**
4
+ * The 3D scene's collision shape (ADR 0028), Rapier's model: alone on an
5
+ * entity it is a fixed wall or floor, created at the entity's position and
6
+ * rotation scaled by `entity.scale`, and moving the entity later does not
7
+ * move it. With a `RigidBody` beside it, it is the body that falls, bounces
8
+ * or walks. `sensor: true` makes it a trigger instead — it detects and never
9
+ * pushes — with the 2D Hitbox's `layer` and `collidesWith` semantics (ADR
10
+ * 0016); with the default mask (`['*']`) a sensor is not told about fixed
11
+ * solid colliders (the level's walls and floors), as a Hitbox never sees a
12
+ * Solid — a mask that names their layer still is. `size` is a box's full
13
+ * extents; `radius` a sphere's or capsule's; `height` a capsule's total
14
+ * end-to-end height, at least twice its radius; `offset` is in the entity's
15
+ * local frame. A Collider in a 2D scene creates nothing; one whose params
16
+ * Rapier cannot take creates no body and warns once (`validate_project`
17
+ * reports them as `invalid-collider-param`).
18
+ */
19
+ export class Collider extends Component {
20
+ static componentName = 'Collider';
21
+ static space = '3d';
22
+ static params = {
23
+ shape: { label: 'Shape', options: [...COLLIDER_SHAPES] },
24
+ size: { label: 'Size', kind: 'vector3' },
25
+ radius: { label: 'Radius', min: 0.01, step: 0.05 },
26
+ height: { label: 'Height', min: 0.01, step: 0.05 },
27
+ offset: { label: 'Offset', kind: 'vector3' },
28
+ sensor: { label: 'Sensor' },
29
+ layer: { label: 'Collision Layer' },
30
+ collidesWith: { label: 'Collision Mask', kind: 'string-list' },
31
+ friction: { label: 'Friction', min: 0, max: 1, step: 0.05 },
32
+ restitution: { label: 'Restitution', min: 0, max: 1, step: 0.05 },
33
+ };
34
+ shape = 'box';
35
+ /** A box's full extents `[x, y, z]`. */
36
+ size = [1, 1, 1];
37
+ /** A sphere's or capsule's radius. */
38
+ radius = 0.5;
39
+ /** A capsule's total end-to-end height. */
40
+ height = 1.8;
41
+ /** Where the shape sits in the entity's local frame. */
42
+ offset = [0, 0, 0];
43
+ /** A trigger: detects overlaps, never pushes or is pushed. */
44
+ sensor = false;
45
+ /** One exact, case-sensitive `^[a-z][a-z0-9-]*$` membership name. */
46
+ layer = 'default';
47
+ /** Outgoing layer interest for `onCollide`; `'*'` means every valid layer and `[]` means none. */
48
+ collidesWith = ['*'];
49
+ friction = 0.5;
50
+ restitution = 0;
51
+ onReady() {
52
+ this.game.physics.attach(this.entity);
53
+ }
54
+ onDestroy() {
55
+ this.game.physics.detach(this.entity);
56
+ }
57
+ }
@@ -1,11 +1,12 @@
1
1
  import { type CollisionPoint, type CollisionShape } from '../collision-shape.js';
2
- import { Component } from '../component.js';
2
+ import { Component, type ComponentSpace } from '../component.js';
3
3
  /**
4
4
  * Reusable linear body: integrates its public velocity and resolves only
5
5
  * against static Solid geometry. Hitbox trigger overlaps remain independent.
6
6
  */
7
7
  export declare class DynamicBody extends Component {
8
8
  static componentName: string;
9
+ static space: ComponentSpace;
9
10
  static params: {
10
11
  vx: {
11
12
  label: string;
@@ -17,6 +17,7 @@ const RECOVERY_ITERATIONS = 24;
17
17
  */
18
18
  export class DynamicBody extends Component {
19
19
  static componentName = 'DynamicBody';
20
+ static space = '2d';
20
21
  static params = {
21
22
  vx: { label: 'x velocity', step: 0.1 },
22
23
  vy: { label: 'y velocity', step: 0.1 },
@@ -1,4 +1,4 @@
1
- import { Component } from '../component.js';
1
+ import { Component, type ComponentSpace } from '../component.js';
2
2
  import { type CollisionPoint, type CollisionShape } from '../collision-shape.js';
3
3
  /**
4
4
  * Trigger collider. A pair is tested when either Hitbox's Collision Mask
@@ -8,6 +8,7 @@ import { type CollisionPoint, type CollisionShape } from '../collision-shape.js'
8
8
  */
9
9
  export declare class Hitbox extends Component {
10
10
  static componentName: string;
11
+ static space: ComponentSpace;
11
12
  static params: {
12
13
  layer: {
13
14
  label: string;
@@ -8,6 +8,7 @@ import { resolveCollisionPoints, } from '../collision-shape.js';
8
8
  */
9
9
  export class Hitbox extends Component {
10
10
  static componentName = 'Hitbox';
11
+ static space = '2d';
11
12
  static params = {
12
13
  layer: { label: 'Collision Layer' },
13
14
  collidesWith: { label: 'Collision Mask', kind: 'string-list' },
@@ -0,0 +1,69 @@
1
+ import { Component, type ComponentSpace } from '../component.js';
2
+ import type { LightField } from '../light-field.js';
3
+ /**
4
+ * A source of 2D light owned by its entity (issue #78, `CONTEXT.md` Light):
5
+ * a radius in logical world units — an ellipse on an isometric screen — a
6
+ * color, an intensity, a falloff that is smooth or split into `bands`, and a
7
+ * shadow edge that is hard or `soft`. Solid tiles stop it unless
8
+ * `castShadows` is off. Lights brighten what the Ambient Light leaves dark;
9
+ * they never darken. A live Light makes its scene draw lit.
10
+ */
11
+ export declare class Light extends Component {
12
+ static componentName: string;
13
+ static space: ComponentSpace;
14
+ static params: {
15
+ radius: {
16
+ label: string;
17
+ min: number;
18
+ step: number;
19
+ };
20
+ color: {
21
+ label: string;
22
+ kind: "color";
23
+ };
24
+ intensity: {
25
+ label: string;
26
+ min: number;
27
+ step: number;
28
+ };
29
+ bands: {
30
+ label: string;
31
+ min: number;
32
+ max: number;
33
+ step: number;
34
+ };
35
+ softness: {
36
+ label: string;
37
+ min: number;
38
+ max: number;
39
+ step: number;
40
+ };
41
+ castShadows: {
42
+ label: string;
43
+ };
44
+ offsetX: {
45
+ label: string;
46
+ };
47
+ offsetY: {
48
+ label: string;
49
+ };
50
+ };
51
+ /** Logical world units from its centre to where it fades out. */
52
+ radius: number;
53
+ /** `0xrrggbb`, multiplied over the art it reaches. */
54
+ color: number;
55
+ intensity: number;
56
+ /** 0 = smooth falloff; N = N equal steps (at most 16). */
57
+ bands: number;
58
+ /** 0 = hard shadow edge; up to 1 = the widest penumbra. */
59
+ softness: number;
60
+ /** Off: solid tiles do not stop this light. */
61
+ castShadows: boolean;
62
+ /** Logical offset from the entity's position, like a Solid's. */
63
+ offsetX: number;
64
+ offsetY: number;
65
+ onReady(): void;
66
+ onDestroy(): void;
67
+ /** This light as the light-map reads it: logical position, clamped params, color channels in 0..1. */
68
+ field(): LightField;
69
+ }
@@ -0,0 +1,60 @@
1
+ import { Component } from '../component.js';
2
+ import { addLight, removeLight } from '../scene-lighting.js';
3
+ const MAX_BANDS = 16;
4
+ const finiteOr = (value, fallback) => (Number.isFinite(value) ? value : fallback);
5
+ /**
6
+ * A source of 2D light owned by its entity (issue #78, `CONTEXT.md` Light):
7
+ * a radius in logical world units — an ellipse on an isometric screen — a
8
+ * color, an intensity, a falloff that is smooth or split into `bands`, and a
9
+ * shadow edge that is hard or `soft`. Solid tiles stop it unless
10
+ * `castShadows` is off. Lights brighten what the Ambient Light leaves dark;
11
+ * they never darken. A live Light makes its scene draw lit.
12
+ */
13
+ export class Light extends Component {
14
+ static componentName = 'Light';
15
+ static space = '2d';
16
+ static params = {
17
+ radius: { label: 'Radius', min: 0, step: 0.25 },
18
+ color: { label: 'Color', kind: 'color' },
19
+ intensity: { label: 'Intensity', min: 0, step: 0.1 },
20
+ bands: { label: 'Bands (0 = smooth)', min: 0, max: MAX_BANDS, step: 1 },
21
+ softness: { label: 'Shadow softness', min: 0, max: 1, step: 0.05 },
22
+ castShadows: { label: 'Cast shadows' },
23
+ offsetX: { label: 'x offset' },
24
+ offsetY: { label: 'y offset' },
25
+ };
26
+ /** Logical world units from its centre to where it fades out. */
27
+ radius = 4;
28
+ /** `0xrrggbb`, multiplied over the art it reaches. */
29
+ color = 0xffffff;
30
+ intensity = 1;
31
+ /** 0 = smooth falloff; N = N equal steps (at most 16). */
32
+ bands = 0;
33
+ /** 0 = hard shadow edge; up to 1 = the widest penumbra. */
34
+ softness = 0;
35
+ /** Off: solid tiles do not stop this light. */
36
+ castShadows = true;
37
+ /** Logical offset from the entity's position, like a Solid's. */
38
+ offsetX = 0;
39
+ offsetY = 0;
40
+ onReady() {
41
+ addLight(this.game.lighting, this);
42
+ }
43
+ onDestroy() {
44
+ removeLight(this.game.lighting, this);
45
+ }
46
+ /** This light as the light-map reads it: logical position, clamped params, color channels in 0..1. */
47
+ field() {
48
+ const color = Number.isFinite(this.color) ? Math.trunc(this.color) & 0xffffff : 0xffffff;
49
+ return {
50
+ x: this.entity.position.x + finiteOr(this.offsetX, 0),
51
+ y: this.entity.position.y + finiteOr(this.offsetY, 0),
52
+ radius: Math.max(0, finiteOr(this.radius, 0)),
53
+ color: [((color >> 16) & 0xff) / 255, ((color >> 8) & 0xff) / 255, (color & 0xff) / 255],
54
+ intensity: Math.max(0, finiteOr(this.intensity, 0)),
55
+ bands: Math.min(MAX_BANDS, Math.max(0, Math.round(finiteOr(this.bands, 0)))),
56
+ softness: Math.min(1, Math.max(0, finiteOr(this.softness, 0))),
57
+ castShadows: this.castShadows !== false,
58
+ };
59
+ }
60
+ }
@@ -0,0 +1,63 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import { Component, type ComponentSpace } from '../component.js';
3
+ export type ModelShape = 'box' | 'sphere' | 'plane';
4
+ /**
5
+ * A 3D thing an entity draws (3D scenes, ADR 0027): a glTF/glb from `src`
6
+ * (a `waica:` uri or a project path, loaded once through `game.assets`) or a
7
+ * primitive `shape` of a `color` and a uniform `size`. With both, `src` wins.
8
+ * Its root hangs under the entity's node, so the entity's position, rotation
9
+ * and scale place it; the glTF keeps the materials it ships with, a
10
+ * primitive has a standard (lit) node material. `color` and `size` follow the
11
+ * inspector live; a new `src` or `shape` rebuilds the content. Destroying it
12
+ * removes the root and disposes only what it created — never the cached glTF.
13
+ * In a 2D scene it creates nothing (`validate_project` reports it).
14
+ */
15
+ export declare class Model extends Component {
16
+ static componentName: string;
17
+ static space: ComponentSpace;
18
+ static params: {
19
+ src: {
20
+ label: string;
21
+ kind: "model";
22
+ };
23
+ shape: {
24
+ label: string;
25
+ options: ModelShape[];
26
+ };
27
+ color: {
28
+ label: string;
29
+ kind: "color";
30
+ };
31
+ size: {
32
+ label: string;
33
+ min: number;
34
+ step: number;
35
+ };
36
+ };
37
+ static transient: string[];
38
+ /** What this Model draws, under the entity's node; null until it is ready. The Pointer ray-casts against it. */
39
+ root: THREE.Group | null;
40
+ private _src;
41
+ private _shape;
42
+ private _color;
43
+ private _size;
44
+ private _primitive;
45
+ /** A `waica:` uri or project path to a `.glb` / `.gltf`; empty draws the primitive `shape`. */
46
+ get src(): string;
47
+ set src(value: string);
48
+ get shape(): ModelShape;
49
+ set shape(value: ModelShape);
50
+ /** `0xrrggbb` of a primitive. */
51
+ get color(): number;
52
+ set color(value: number);
53
+ /** Uniform scale of a primitive: the box side, sphere diameter or plane side in world units. */
54
+ get size(): number;
55
+ set size(value: number);
56
+ onReady(): void;
57
+ onDestroy(): void;
58
+ private build;
59
+ private buildPrimitive;
60
+ private rebuild;
61
+ /** Empties the root and disposes the primitive this Model created; a glTF's clone only leaves the graph. */
62
+ private clear;
63
+ }
@@ -0,0 +1,117 @@
1
+ import * as THREE from 'three/webgpu';
2
+ import { Component } from '../component.js';
3
+ const SHAPES = ['box', 'sphere', 'plane'];
4
+ /** One unit of the shape: a box of side 1, a sphere of diameter 1, a plane of side 1 lying flat on the ground (normal up). */
5
+ function primitiveGeometry(shape) {
6
+ switch (shape) {
7
+ case 'sphere':
8
+ return new THREE.SphereGeometry(0.5, 32, 16);
9
+ case 'plane':
10
+ return new THREE.PlaneGeometry(1, 1).rotateX(-Math.PI / 2);
11
+ case 'box':
12
+ return new THREE.BoxGeometry(1, 1, 1);
13
+ }
14
+ }
15
+ /**
16
+ * A 3D thing an entity draws (3D scenes, ADR 0027): a glTF/glb from `src`
17
+ * (a `waica:` uri or a project path, loaded once through `game.assets`) or a
18
+ * primitive `shape` of a `color` and a uniform `size`. With both, `src` wins.
19
+ * Its root hangs under the entity's node, so the entity's position, rotation
20
+ * and scale place it; the glTF keeps the materials it ships with, a
21
+ * primitive has a standard (lit) node material. `color` and `size` follow the
22
+ * inspector live; a new `src` or `shape` rebuilds the content. Destroying it
23
+ * removes the root and disposes only what it created — never the cached glTF.
24
+ * In a 2D scene it creates nothing (`validate_project` reports it).
25
+ */
26
+ export class Model extends Component {
27
+ static componentName = 'Model';
28
+ static space = '3d';
29
+ static params = {
30
+ src: { label: 'Model file', kind: 'model' },
31
+ shape: { label: 'Shape', options: [...SHAPES] },
32
+ color: { label: 'Color', kind: 'color' },
33
+ size: { label: 'Size', min: 0.01, step: 0.1 },
34
+ };
35
+ static transient = ['root'];
36
+ /** What this Model draws, under the entity's node; null until it is ready. The Pointer ray-casts against it. */
37
+ root = null;
38
+ _src = '';
39
+ _shape = 'box';
40
+ _color = 0xffffff;
41
+ _size = 1;
42
+ _primitive = null;
43
+ /** A `waica:` uri or project path to a `.glb` / `.gltf`; empty draws the primitive `shape`. */
44
+ get src() {
45
+ return this._src;
46
+ }
47
+ set src(value) {
48
+ this._src = value;
49
+ this.rebuild();
50
+ }
51
+ get shape() {
52
+ return this._shape;
53
+ }
54
+ set shape(value) {
55
+ this._shape = SHAPES.includes(value) ? value : 'box';
56
+ this.rebuild();
57
+ }
58
+ /** `0xrrggbb` of a primitive. */
59
+ get color() {
60
+ return this._color;
61
+ }
62
+ set color(value) {
63
+ this._color = value;
64
+ this._primitive?.mesh.material.color.setHex(value);
65
+ }
66
+ /** Uniform scale of a primitive: the box side, sphere diameter or plane side in world units. */
67
+ get size() {
68
+ return this._size;
69
+ }
70
+ set size(value) {
71
+ this._size = value;
72
+ this._primitive?.mesh.scale.setScalar(value);
73
+ }
74
+ onReady() {
75
+ // Draws only in a 3D scene, like Sun and PointLight: 2.5D is a later block, and a lit-less mesh would come out black.
76
+ if (this.game.space !== '3d')
77
+ return;
78
+ this.root = new THREE.Group();
79
+ this.root.name = 'waica:model';
80
+ this.entity.node.add(this.root);
81
+ this.build();
82
+ }
83
+ onDestroy() {
84
+ this.clear();
85
+ this.root?.removeFromParent();
86
+ }
87
+ build() {
88
+ if (!this.root)
89
+ return;
90
+ if (this._src)
91
+ this.root.add(this.game.assets.model(this._src).root);
92
+ else
93
+ this.buildPrimitive(this.root);
94
+ }
95
+ buildPrimitive(root) {
96
+ const material = new THREE.MeshStandardNodeMaterial({ color: this._color });
97
+ const mesh = new THREE.Mesh(primitiveGeometry(this._shape), material);
98
+ mesh.scale.setScalar(this._size);
99
+ root.add(mesh);
100
+ this._primitive = { mesh };
101
+ }
102
+ rebuild() {
103
+ if (!this.root)
104
+ return;
105
+ this.clear();
106
+ this.build();
107
+ }
108
+ /** Empties the root and disposes the primitive this Model created; a glTF's clone only leaves the graph. */
109
+ clear() {
110
+ this.root?.clear();
111
+ if (!this._primitive)
112
+ return;
113
+ this._primitive.mesh.geometry.dispose();
114
+ this._primitive.mesh.material.dispose();
115
+ this._primitive = null;
116
+ }
117
+ }
@@ -1,4 +1,4 @@
1
- import { Component } from '../component.js';
1
+ import { Component, type ComponentSpace } from '../component.js';
2
2
  import type { YSortBatchParticipant, YSortEntry } from '../render-sort.js';
3
3
  export type ParticleVector = [number, number];
4
4
  export type ParticleSpace = 'world' | 'local';
@@ -8,6 +8,7 @@ export type ParticleBlend = 'normal' | 'additive';
8
8
  /** A fixed-capacity source of short-lived, batched 2D particles. */
9
9
  export declare class ParticleEmitter extends Component implements YSortBatchParticipant {
10
10
  static componentName: string;
11
+ static space: ComponentSpace;
11
12
  static displayName: string;
12
13
  static params: {
13
14
  rate: {
@@ -107,6 +108,9 @@ export declare class ParticleEmitter extends Component implements YSortBatchPart
107
108
  label: string;
108
109
  step: number;
109
110
  };
111
+ emissive: {
112
+ label: string;
113
+ };
110
114
  };
111
115
  rate: number;
112
116
  emitting: boolean;
@@ -144,6 +148,9 @@ export declare class ParticleEmitter extends Component implements YSortBatchPart
144
148
  private _layer;
145
149
  get layer(): number;
146
150
  set layer(value: number);
151
+ private _emissive;
152
+ get emissive(): boolean;
153
+ set emissive(value: boolean);
147
154
  private _batch?;
148
155
  private _unsubscribeRenderSync?;
149
156
  private _emissionRemainder;
@@ -1,9 +1,11 @@
1
1
  import { Component } from '../component.js';
2
2
  import { ParticleBatch, } from '../particle-batch.js';
3
3
  import { sceneDrainsOf } from '../scene-drains.js';
4
+ import { setEmissive } from '../render-layers.js';
4
5
  /** A fixed-capacity source of short-lived, batched 2D particles. */
5
6
  export class ParticleEmitter extends Component {
6
7
  static componentName = 'ParticleEmitter';
8
+ static space = '2d';
7
9
  static displayName = 'Particle Emitter';
8
10
  static params = {
9
11
  rate: { label: 'Rate', min: 0 },
@@ -30,6 +32,7 @@ export class ParticleEmitter extends Component {
30
32
  pixelArt: { label: 'Pixel art' },
31
33
  blend: { label: 'Blend', options: ['normal', 'additive'] },
32
34
  layer: { label: 'Layer', step: 1 },
35
+ emissive: { label: 'Emissive' },
33
36
  };
34
37
  rate = 0;
35
38
  emitting = true;
@@ -116,6 +119,16 @@ export class ParticleEmitter extends Component {
116
119
  this._batch?.setLayer(next);
117
120
  this._batch?.sync(this.renderContext());
118
121
  }
122
+ // Emissive (issue #78 CA-10): never darkened by the light-map.
123
+ _emissive = false;
124
+ get emissive() {
125
+ return this._emissive;
126
+ }
127
+ set emissive(value) {
128
+ this._emissive = value === true;
129
+ if (this._batch)
130
+ setEmissive(this._batch.mesh, this._emissive);
131
+ }
119
132
  _batch;
120
133
  _unsubscribeRenderSync;
121
134
  _emissionRemainder = 0;
@@ -133,6 +146,7 @@ export class ParticleEmitter extends Component {
133
146
  assets: this.game.assets,
134
147
  });
135
148
  this._batch.setTexture(this.texture, this.pixelArt);
149
+ setEmissive(this._batch.mesh, this._emissive);
136
150
  this.game.scene.add(this._batch.mesh);
137
151
  this._batch.sync(this.renderContext());
138
152
  this._unsubscribeRenderSync = this.game.onUpdate(() => this._batch?.sync(this.renderContext()));