lecodes-sdk 0.20.0 → 1.0.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 (114) hide show
  1. package/dist/global.d.ts +48 -5
  2. package/dist/inject.js +361 -260
  3. package/dist/types/audio/Bus.d.ts +45 -0
  4. package/dist/types/audio/Sound.d.ts +28 -0
  5. package/dist/types/audio/Voice.d.ts +27 -0
  6. package/dist/types/audio/audio.d.ts +83 -0
  7. package/dist/types/audio/support.d.ts +1 -0
  8. package/dist/types/canvas/Canvas.d.ts +2 -0
  9. package/dist/types/gl/DecalSet.d.ts +148 -0
  10. package/dist/types/gl/Geometry.d.ts +17 -0
  11. package/dist/types/gl/Light.d.ts +7 -0
  12. package/dist/types/gl/Lightmap.d.ts +9 -0
  13. package/dist/types/gl/Material.d.ts +90 -2
  14. package/dist/types/gl/Mesh.d.ts +18 -1
  15. package/dist/types/gl/Model.d.ts +34 -0
  16. package/dist/types/gl/Particles.d.ts +13 -0
  17. package/dist/types/gl/Scene.d.ts +23 -0
  18. package/dist/types/gl/Texture.d.ts +29 -1
  19. package/dist/types/gl/animation/AnimationClip.d.ts +25 -12
  20. package/dist/types/gl/animation/DynamicBone.d.ts +173 -0
  21. package/dist/types/gl/{IK.d.ts → animation/IK.d.ts} +4 -4
  22. package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
  23. package/dist/types/gl/animation/core.d.ts +15 -10
  24. package/dist/types/gl/audio/AudioSource.d.ts +60 -0
  25. package/dist/types/gl/audio/AudioZone.d.ts +32 -0
  26. package/dist/types/gl/audio/SceneAudio.d.ts +11 -0
  27. package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
  28. package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
  29. package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
  30. package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
  31. package/dist/types/gl/{Ragdoll.d.ts → physics/Ragdoll.d.ts} +4 -4
  32. package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
  33. package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
  34. package/dist/types/gl/state.d.ts +0 -1
  35. package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +6 -6
  36. package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
  37. package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
  38. package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
  39. package/dist/types/inject.d.ts +33 -21
  40. package/dist/types/runtime/files.d.ts +24 -1
  41. package/dist/types/runtime/input.d.ts +11 -0
  42. package/dist/types/scene/defineScene.d.ts +10 -3
  43. package/dist/types/ui/UIImage.d.ts +15 -5
  44. package/dist/types.json +1 -1
  45. package/package.json +1 -1
  46. package/prompts/README.md +142 -142
  47. package/prompts/dist/2d-game.md +408 -197
  48. package/prompts/dist/3d-app.md +491 -166
  49. package/prompts/dist/ar-app.md +373 -163
  50. package/prompts/dist/design.md +83 -87
  51. package/prompts/dist/ui-app.md +325 -136
  52. package/src/audio/Bus.ts +102 -0
  53. package/src/audio/Sound.ts +96 -0
  54. package/src/audio/Voice.ts +102 -0
  55. package/src/audio/audio.ts +161 -0
  56. package/src/audio/support.ts +6 -0
  57. package/src/bridges.d.ts +279 -32
  58. package/src/canvas/Canvas.ts +21 -0
  59. package/src/compile/__tests__/compile.test.ts +11 -0
  60. package/src/compile/compileProject.ts +30 -15
  61. package/src/compile/header.ts +6 -3
  62. package/src/compile/index.ts +4 -0
  63. package/src/compile/sceneEditor.ts +42 -1
  64. package/src/core/Aspect.ts +33 -8
  65. package/src/g2/Scene2D.ts +7 -0
  66. package/src/gl/CameraPlace.ts +52 -52
  67. package/src/gl/DecalSet.ts +360 -0
  68. package/src/gl/Geometry.ts +348 -279
  69. package/src/gl/Light.ts +16 -0
  70. package/src/gl/Lightmap.ts +35 -7
  71. package/src/gl/Material.ts +173 -4
  72. package/src/gl/Mesh.ts +120 -83
  73. package/src/gl/Model.ts +33 -1
  74. package/src/gl/Node.ts +1 -1
  75. package/src/gl/Particles.ts +21 -3
  76. package/src/gl/Scene.ts +41 -7
  77. package/src/gl/Texture.ts +43 -3
  78. package/src/gl/animation/AnimationClip.ts +43 -20
  79. package/src/gl/animation/Animator.ts +4 -3
  80. package/src/gl/animation/DynamicBone.ts +459 -0
  81. package/src/gl/{IK.ts → animation/IK.ts} +4 -4
  82. package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
  83. package/src/gl/animation/core.ts +20 -15
  84. package/src/gl/audio/AudioSource.ts +113 -0
  85. package/src/gl/audio/AudioZone.ts +75 -0
  86. package/src/gl/audio/SceneAudio.ts +26 -0
  87. package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
  88. package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
  89. package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
  90. package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
  91. package/src/gl/{Ragdoll.ts → physics/Ragdoll.ts} +272 -270
  92. package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
  93. package/src/gl/{Trigger.ts → physics/Trigger.ts} +2 -2
  94. package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
  95. package/src/gl/scenarios.ts +291 -291
  96. package/src/gl/state.ts +1 -1
  97. package/src/gl/{Terrain.ts → terrain/Terrain.ts} +10 -10
  98. package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
  99. package/src/gl/vehicle/Vehicle.ts +666 -0
  100. package/src/gl/vehicle/Wheel.ts +290 -0
  101. package/src/inject.ts +226 -212
  102. package/src/runtime/files.ts +32 -2
  103. package/src/runtime/input.ts +6 -1
  104. package/src/scene/defineScene.ts +26 -10
  105. package/src/scene/gizmos.ts +148 -148
  106. package/src/scene/level.ts +2 -2
  107. package/src/ui/UIImage.ts +21 -7
  108. package/dist/types/gl/Gearbox.d.ts +0 -86
  109. package/dist/types/gl/Vehicle.d.ts +0 -191
  110. package/dist/types/gl/Wheel.d.ts +0 -95
  111. package/src/gl/Gearbox.ts +0 -212
  112. package/src/gl/Vehicle.ts +0 -473
  113. package/src/gl/Wheel.ts +0 -240
  114. /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
@@ -0,0 +1,113 @@
1
+ // A 3D sound emitter, as an aspect on a Node (docs/audio-plan.md §2). The engine follows the node's
2
+ // world pose every frame (position, facing for the cone, velocity for doppler); voices played
3
+ // through it are spatialized against the scene's listener (scene.audio.listener, the camera by
4
+ // default). Parented, animated and physics-driven nodes just work.
5
+ //
6
+ // const gun = new Node().aspect(AudioSource, { minDistance: 1, maxDistance: 40 })
7
+ // gun.audio.play(shot, { pitch: 0.95 + Math.random() * 0.1 })
8
+ // const hum = car.audio.play(engineLoop, { loop: true }); hum.pitch = 0.8 + rpm * 0.6
9
+ // speaker.aspect(AudioSource, { cone: { inner: 60, outer: 120, outerGain: 0.3 } }) // facing −Z
10
+ // enemy.aspect(AudioSource, { occlusion: true }) // a wall between it and the listener muffles it
11
+ //
12
+ // Without host audio the aspect is inert: `play` returns an inert Voice, nothing throws.
13
+
14
+ import { Aspect } from "../../core/Aspect"
15
+ import type { FieldMeta } from "../../core/fields"
16
+ import { audio, sourceParams, type PlaySoundOptions, type Rolloff } from "../../audio/audio"
17
+ import type { Sound } from "../../audio/Sound"
18
+ import { audioSupported } from "../../audio/support"
19
+ import type { Voice } from "../../audio/Voice"
20
+ import type { Node } from "../Node"
21
+
22
+ export type AudioCone = {
23
+ /** Degrees of full volume around the node's −Z. */
24
+ inner: number
25
+ /** Degrees where the volume has fallen to `outerGain`. */
26
+ outer: number
27
+ /** Volume behind the source, 0 … 1. */
28
+ outerGain?: number
29
+ }
30
+
31
+ export class AudioSource extends Aspect<"audio", Node> {
32
+ static readonly aspect = "audio"
33
+
34
+ static fields: FieldMeta<AudioSource> = {
35
+ minDistance: { min: 0.05, max: 100, step: 0.05 },
36
+ maxDistance: { min: 0.5, max: 1000, step: 0.5 },
37
+ doppler: { min: 0, max: 1, step: 0.05 },
38
+ spread: { min: 0, max: 1, step: 0.05 },
39
+ }
40
+
41
+ private _minDistance = 1
42
+ private _maxDistance = 50
43
+ private _rolloff: Rolloff = "inverse"
44
+ private _cone: AudioCone | null = null
45
+ private _doppler = 0
46
+ private _spread = 0
47
+ private _occlusion = false
48
+ private _bus = "sfx"
49
+ /** @internal engine source id (0 = not attached / no audio). */
50
+ _id = 0
51
+
52
+ /** Metres of full volume around the node. Default 1. */
53
+ get minDistance(): number { return this._minDistance }
54
+ set minDistance(v: number) { this._minDistance = v; this._push() }
55
+ /** Metres beyond which the source no longer gets quieter. Default 50. */
56
+ get maxDistance(): number { return this._maxDistance }
57
+ set maxDistance(v: number) { this._maxDistance = v; this._push() }
58
+ /** `inverse` (default), `linear`, `exponential`, `none`. */
59
+ get rolloff(): Rolloff { return this._rolloff }
60
+ set rolloff(v: Rolloff) { this._rolloff = v; this._push() }
61
+ /** Directional source along the node's −Z; null = omnidirectional (default). */
62
+ get cone(): AudioCone | null { return this._cone }
63
+ set cone(v: AudioCone | null) { this._cone = v; this._push() }
64
+ /** Doppler amount 0 … 1 (0 = off, the default) — needs the node to actually move. */
65
+ get doppler(): number { return this._doppler }
66
+ set doppler(v: number) { this._doppler = v; this._push() }
67
+ /** 0 = pin-point panning (default) … 1 = the same on every speaker (a big, close source). */
68
+ get spread(): number { return this._spread }
69
+ set spread(v: number) { this._spread = v; this._push() }
70
+ /** The engine raycasts listener → source (against solid bodies) and muffles the voices when
71
+ * something is in the way. Default false. */
72
+ get occlusion(): boolean { return this._occlusion }
73
+ set occlusion(v: boolean) { this._occlusion = v; this._push() }
74
+ /** Default bus for voices on this source. Default `sfx`. */
75
+ get bus(): string { return this._bus }
76
+ set bus(v: string) { this._bus = v; this._push() }
77
+
78
+ /** Live voices on this source. */
79
+ get voices(): number { return this._id && audioSupported ? _creatorAudio.sourceVoices(this._id) : 0 }
80
+
81
+ onAttach(): void {
82
+ if (!audioSupported) return
83
+ this._id = _creatorAudio.createSource()
84
+ if (!this._id) return
85
+ _creatorAudio.attachSource(this._id, this.node.id)
86
+ this._push()
87
+ }
88
+
89
+ onDetach(): void {
90
+ if (this._id && audioSupported) _creatorAudio.destroySource(this._id)
91
+ this._id = 0
92
+ }
93
+
94
+ /** Play a clip from this node. */
95
+ play(sound: Sound, options: PlaySoundOptions = {}): Voice {
96
+ return audio._play(sound, this._id, options)
97
+ }
98
+
99
+ /** Stop every voice on this source (fade in seconds). */
100
+ stopAll(fade = 0): void {
101
+ if (this._id && audioSupported) _creatorAudio.stopSource(this._id, fade)
102
+ }
103
+
104
+ private _push(): void {
105
+ if (!this._id || !audioSupported) return
106
+ const cone = this._cone
107
+ _creatorAudio.setSourceParams(this._id, sourceParams({
108
+ minDistance: this._minDistance, maxDistance: this._maxDistance, rolloff: this._rolloff,
109
+ coneInner: cone ? cone.inner : 360, coneOuter: cone ? cone.outer : 360, coneOuterGain: cone ? cone.outerGain ?? 0.25 : 1,
110
+ doppler: this._doppler, spread: this._spread, occlusion: this._occlusion, bus: audio._busId(this._bus),
111
+ }))
112
+ }
113
+ }
@@ -0,0 +1,75 @@
1
+ // A reverb zone, as an aspect on a Node (docs/audio-plan.md §2): a box or sphere volume that
2
+ // follows the node; while the listener is inside, the zone's reverb blends onto a bus (the bus's
3
+ // own reverb outside it). Zones nest: the smallest one containing the listener wins. `blend` is
4
+ // how many metres inside the border the crossfade takes — walking through a doorway is smooth.
5
+ //
6
+ // new Node().aspect(AudioZone, { box: [6, 2.5, 6], reverb: 'hall', blend: 2 })
7
+ // corridor.aspect(Shape, { box: [1, 1.5, 8] }).aspect(AudioZone, { reverb: 'bathroom' }) // the Shape's box
8
+ //
9
+ // Sizes follow Shape's convention: `box` = HALF-extents, `sphere` = radius, in the node's local
10
+ // units (the node's scale applies). Without host audio the aspect is inert.
11
+
12
+ import { Aspect } from "../../core/Aspect"
13
+ import { reverbBlock, type ReverbParams, type ReverbPreset } from "../../audio/Bus"
14
+ import { audio } from "../../audio/audio"
15
+ import { audioSupported } from "../../audio/support"
16
+ import { cx, cy, cz, type Vec3Like } from "../../math/vec"
17
+ import type { Node } from "../Node"
18
+ import { Shape } from "../physics/Shape"
19
+
20
+ export class AudioZone extends Aspect<"audioZone", Node> {
21
+ static readonly aspect = "audioZone"
22
+
23
+ private _box: Vec3Like | null = null
24
+ private _sphere: number | null = null
25
+ private _reverb: ReverbPreset | ReverbParams = "room"
26
+ private _blend = 1
27
+ private _bus = "sfx"
28
+ /** @internal engine zone id. */
29
+ _id = 0
30
+ /** The engine's zone id — what `audio.stats.listenerZone` reports while the listener is inside. */
31
+ get id(): number { return this._id }
32
+
33
+ /** Half-extents [hx, hy, hz]; falls back to the node's Shape box, then a 1 m cube. */
34
+ get box(): Vec3Like | null { return this._box }
35
+ set box(v: Vec3Like | null) { this._box = v; if (v) this._sphere = null; this._push() }
36
+ /** Radius; falls back to the node's Shape sphere. */
37
+ get sphere(): number | null { return this._sphere }
38
+ set sphere(v: number | null) { this._sphere = v; if (v !== null) this._box = null; this._push() }
39
+ /** A preset name or explicit params (see Bus.reverb). */
40
+ get reverb(): ReverbPreset | ReverbParams { return this._reverb }
41
+ set reverb(v: ReverbPreset | ReverbParams) { this._reverb = v; this._push() }
42
+ /** Crossfade depth in metres inside the border (0 = a hard edge). Default 1. */
43
+ get blend(): number { return this._blend }
44
+ set blend(v: number) { this._blend = v; this._push() }
45
+ /** The bus the zone's reverb rides on. Default `sfx`. */
46
+ get bus(): string { return this._bus }
47
+ set bus(v: string) { this._bus = v; this._push() }
48
+
49
+ onAttach(): void {
50
+ if (!audioSupported) return
51
+ this._id = _creatorAudio.createZone()
52
+ if (!this._id) return
53
+ _creatorAudio.attachZone(this._id, this.node.id)
54
+ this._push()
55
+ }
56
+
57
+ onDetach(): void {
58
+ if (this._id && audioSupported) _creatorAudio.destroyZone(this._id)
59
+ this._id = 0
60
+ }
61
+
62
+ private _push(): void {
63
+ if (!this._id || !audioSupported) return
64
+ let box = this._box, sphere = this._sphere
65
+ if (!box && sphere === null) {
66
+ const shape = this.node.get(Shape)
67
+ if (shape?.sphere !== undefined) sphere = shape.sphere
68
+ else if (shape?.box) box = shape.box
69
+ }
70
+ const dims = sphere !== null
71
+ ? new Float32Array([ sphere * 2, sphere * 2, sphere * 2 ])
72
+ : new Float32Array(box ? [ cx(box) * 2, cy(box) * 2, cz(box) * 2 ] : [ 2, 2, 2 ])
73
+ _creatorAudio.setZone(this._id, sphere !== null ? 1 : 0, dims, this._blend, audio._busId(this._bus), reverbBlock(this._reverb))
74
+ }
75
+ }
@@ -0,0 +1,26 @@
1
+ // `scene.audio` (docs/audio-plan.md §2): the listener and the global 3D knobs. The listener is the
2
+ // scene's active camera by default — a `CameraPlace` switch or a follow rig changes nothing, the
3
+ // engine reads whatever camera it renders with. Set a node to listen from somewhere else (the
4
+ // player's head in a third-person game, so sounds pan around the character, not the camera).
5
+
6
+ import { audioSupported } from "../../audio/support"
7
+ import type { Node } from "../Node"
8
+
9
+ export class SceneAudio {
10
+ private _listener: Node | null = null
11
+ private _doppler = 1
12
+
13
+ /** The node the engine listens from; null (default) = the active camera. */
14
+ get listener(): Node | null { return this._listener }
15
+ set listener(node: Node | null) {
16
+ this._listener = node
17
+ if (audioSupported) _creatorAudio.setListener(node ? node.id : 0)
18
+ }
19
+
20
+ /** Multiplies every source's doppler amount (0 = off everywhere). Default 1. */
21
+ get dopplerFactor(): number { return this._doppler }
22
+ set dopplerFactor(v: number) {
23
+ this._doppler = v
24
+ if (audioSupported) _creatorAudio.setListenerOptions(v)
25
+ }
26
+ }
@@ -20,12 +20,12 @@
20
20
  // Without a loaded NavMesh the aspect is inert — `goTo` returns false, nothing throws — so a
21
21
  // project stays runnable on a host without navigation.
22
22
 
23
- import { Aspect } from "../core/Aspect"
24
- import type { FieldMeta } from "../core/fields"
25
- import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
26
- import { CharacterController } from "./CharacterController"
23
+ import { Aspect } from "../../core/Aspect"
24
+ import type { FieldMeta } from "../../core/fields"
25
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
26
+ import { CharacterController } from "../physics/CharacterController"
27
27
  import { NavCrowd, NavMesh, NAV_AGENT, NAV_READ } from "./NavMesh"
28
- import type { Node } from "./Node"
28
+ import type { Node } from "../Node"
29
29
 
30
30
  export type NavAgentState = "idle" | "moving" | "arrived" | "blocked" | "offmesh"
31
31
  export type NavAgentAvoidance = "off" | "low" | "medium" | "high"
@@ -18,14 +18,14 @@
18
18
  // (creator-nav) is the algorithms and the per-step hot path: ONE crowd update + ONE flat read per
19
19
  // fixed step for every agent in the scene (`NavCrowd`, a system the mesh attaches to its scene).
20
20
 
21
- import { fetch } from "../runtime/fetch"
22
- import { System } from "../core/Aspect"
23
- import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
24
- import { Geometry } from "./Geometry"
25
- import { Material } from "./Material"
26
- import { Mesh } from "./Mesh"
27
- import type { Node } from "./Node"
28
- import type { Scene } from "./Scene"
21
+ import { fetch } from "../../runtime/fetch"
22
+ import { System } from "../../core/Aspect"
23
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
24
+ import { Geometry } from "../Geometry"
25
+ import { Material } from "../Material"
26
+ import { Mesh } from "../Mesh"
27
+ import type { Node } from "../Node"
28
+ import type { Scene } from "../Scene"
29
29
  import type { NavAgent } from "./NavAgent"
30
30
 
31
31
  // Fixed-order layouts — keep in step with creator-nav/creator-nav.h (CNAV_AGENT_* / CNAV_READ_*).
@@ -38,12 +38,12 @@
38
38
  // Note: it collides with solid bodies but PASSES THROUGH triggers, and (v1) is not itself detected by
39
39
  // triggers and isn't pointer-pickable while active.
40
40
 
41
- import { Aspect } from "../core/Aspect"
42
- import type { FieldMeta } from "../core/fields"
43
- import { Vec3, cx, cy, cz, type Vec2Like, type Vec3Like } from "../math/vec"
44
- import type { CompAxis, CompWriter } from "../core/compWrite"
41
+ import { Aspect } from "../../core/Aspect"
42
+ import type { FieldMeta } from "../../core/fields"
43
+ import { Vec3, cx, cy, cz, type Vec2Like, type Vec3Like } from "../../math/vec"
44
+ import type { CompAxis, CompWriter } from "../../core/compWrite"
45
45
  import { Shape } from "./Shape"
46
- import type { Node } from "./Node"
46
+ import type { Node } from "../Node"
47
47
 
48
48
  /** Where the character's feet are, as reported by the solver after the last step. */
49
49
  export type GroundState = "ground" | "slope" | "unsupported" | "air"
@@ -11,11 +11,11 @@
11
11
  // velocity / applyImpulse. Reading node.position is free (native already wrote it). A `position` write
12
12
  // is a TELEPORT that reaches the body (Shape routes it), which is how a kinematic body is placed too.
13
13
 
14
- import { Aspect } from "../core/Aspect"
15
- import type { FieldMeta } from "../core/fields"
16
- import type { CompAxis, CompWriter } from "../core/compWrite"
17
- import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
18
- import { nodeRegistry, type Node } from "./Node"
14
+ import { Aspect } from "../../core/Aspect"
15
+ import type { FieldMeta } from "../../core/fields"
16
+ import type { CompAxis, CompWriter } from "../../core/compWrite"
17
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../../math/vec"
18
+ import { nodeRegistry, type Node } from "../Node"
19
19
  import { Shape } from "./Shape"
20
20
 
21
21
  export type MotionType = "static" | "dynamic" | "kinematic"
@@ -95,6 +95,7 @@ export class Physics extends Aspect<"physics", Node> implements CompWriter {
95
95
  }
96
96
 
97
97
  private _bodyId = 0
98
+ private static _refusedWarned = false
98
99
 
99
100
  onAttach(): void {
100
101
  if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
@@ -107,6 +108,12 @@ export class Physics extends Aspect<"physics", Node> implements CompWriter {
107
108
  }
108
109
  const shapeId = shape._claim() // drop the pick-only body; reuse its shape
109
110
  this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, MOTION[this.motion], this.mass, false, false)
111
+ // The engine refuses a body past its limit (4 096 unless configured) — silently, and a level that
112
+ // crosses it loses whatever loads LAST (the terrain, streamed in after the props): say so once.
113
+ if (!this._bodyId && shapeId && !Physics._refusedWarned) {
114
+ Physics._refusedWarned = true
115
+ console.warn(`Physics: the engine refused a body for "${this.node.name}" — the body limit is reached? Raise it before the level loads: Physics.configure({ maxBodies })`)
116
+ }
110
117
  // A fresh body already carries DEFAULT_FRICTION host-side, so only a NAMED surface has to
111
118
  // travel — the common case costs no bridge call at all.
112
119
  if (this._friction !== DEFAULT_FRICTION) _creator.physicsSetFriction?.(this._bodyId, this._friction)