lecodes-cli 0.18.2 → 0.19.2

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 (224) hide show
  1. package/dist/index.js +1866 -301
  2. package/package.json +14 -12
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk-types.json +1 -1
  5. package/src/api.ts +302 -0
  6. package/src/browserAuth.ts +87 -0
  7. package/src/cmgenTool.ts +104 -0
  8. package/src/commands/app.ts +891 -0
  9. package/src/commands/appAndroid.ts +603 -0
  10. package/src/commands/appDesktop.ts +326 -0
  11. package/src/commands/appDesktopMac.ts +470 -0
  12. package/src/commands/appIcon.ts +187 -0
  13. package/src/commands/appShared.ts +448 -0
  14. package/src/commands/appTemplates.ts +883 -0
  15. package/src/commands/appTemplatesAndroid.ts +599 -0
  16. package/src/commands/appTemplatesGradlew.ts +9 -0
  17. package/src/commands/assets.ts +28 -0
  18. package/src/commands/clone.ts +60 -0
  19. package/src/commands/compile.ts +253 -0
  20. package/src/commands/create.ts +59 -0
  21. package/src/commands/design.ts +863 -0
  22. package/src/commands/designTemplates.ts +11 -0
  23. package/src/commands/desktop.ts +122 -0
  24. package/src/commands/dev.ts +214 -0
  25. package/src/commands/diff.ts +59 -0
  26. package/src/commands/init.ts +191 -0
  27. package/src/commands/install.ts +198 -0
  28. package/src/commands/lightmap.ts +290 -0
  29. package/src/commands/link.ts +147 -0
  30. package/src/commands/login.ts +24 -0
  31. package/src/commands/navmesh.ts +225 -0
  32. package/src/commands/pn.ts +305 -0
  33. package/src/commands/projectTemplates.ts +129 -0
  34. package/src/commands/pull.ts +109 -0
  35. package/src/commands/push.ts +173 -0
  36. package/src/commands/render.ts +414 -0
  37. package/src/commands/scene.ts +148 -0
  38. package/src/commands/shaders.ts +157 -0
  39. package/src/commands/status.ts +29 -0
  40. package/src/commands/test.ts +370 -0
  41. package/src/commands/thumbs.ts +178 -0
  42. package/src/commands/types.ts +87 -0
  43. package/src/commands/update.ts +190 -0
  44. package/src/compile/assetIcons.ts +214 -0
  45. package/src/compile/collect.ts +46 -0
  46. package/src/compile/collectLocal.ts +37 -0
  47. package/src/compile/designCompile.ts +104 -0
  48. package/src/compile/fonts.ts +156 -0
  49. package/src/compile/headlessBundle.ts +129 -0
  50. package/src/compile/nativeStack.ts +21 -0
  51. package/src/compile/projectCompile.ts +109 -0
  52. package/src/compile/sceneCompile.ts +113 -0
  53. package/src/compile/screenEntry.ts +127 -0
  54. package/src/compile/shaders.ts +245 -0
  55. package/src/config.ts +42 -0
  56. package/src/designMeta.ts +35 -0
  57. package/src/desktopRenderer.ts +532 -0
  58. package/src/desktopScript.ts +276 -0
  59. package/src/dev/clientTemplates.ts +160 -0
  60. package/src/dev/devServer.ts +292 -0
  61. package/src/dev/wsServer.ts +144 -0
  62. package/src/distRoot.ts +20 -0
  63. package/src/ignore.ts +163 -0
  64. package/src/index.ts +491 -0
  65. package/src/lecodes-3d-editor.d.ts +41 -0
  66. package/src/lecodes-assets.d.ts +7 -0
  67. package/src/lecodes-design.d.ts +191 -0
  68. package/src/lecodes-renderer.d.ts +131 -0
  69. package/src/localFiles.ts +144 -0
  70. package/src/manifest.ts +46 -0
  71. package/src/matcTool.ts +137 -0
  72. package/src/peers.ts +40 -0
  73. package/src/project.ts +61 -0
  74. package/src/projectEnv.ts +94 -0
  75. package/src/qrcode-terminal.d.ts +9 -0
  76. package/src/releases.ts +125 -0
  77. package/src/serverDiff.ts +78 -0
  78. package/src/textDiff.ts +103 -0
  79. package/src/types.ts +0 -0
  80. package/src/util.ts +146 -0
  81. package/runtime/sdk/animate/animate.ts +0 -238
  82. package/runtime/sdk/animate/bezier.ts +0 -138
  83. package/runtime/sdk/animate/easings.ts +0 -126
  84. package/runtime/sdk/canvas/Canvas.ts +0 -305
  85. package/runtime/sdk/core/Aspect.ts +0 -512
  86. package/runtime/sdk/core/InspectorUI.ts +0 -212
  87. package/runtime/sdk/core/color.ts +0 -66
  88. package/runtime/sdk/core/compWrite.ts +0 -42
  89. package/runtime/sdk/core/events.ts +0 -38
  90. package/runtime/sdk/core/fields.ts +0 -120
  91. package/runtime/sdk/core/registry.ts +0 -23
  92. package/runtime/sdk/core/signals.ts +0 -277
  93. package/runtime/sdk/core/time.ts +0 -81
  94. package/runtime/sdk/g2/Camera2D.ts +0 -40
  95. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  96. package/runtime/sdk/g2/Node2D.ts +0 -267
  97. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  98. package/runtime/sdk/g2/Physics2D.ts +0 -346
  99. package/runtime/sdk/g2/Scene2D.ts +0 -209
  100. package/runtime/sdk/g2/Shape2D.ts +0 -259
  101. package/runtime/sdk/g2/Sprite.ts +0 -89
  102. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  103. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  104. package/runtime/sdk/g2/Texture2D.ts +0 -47
  105. package/runtime/sdk/g2/Tilemap.ts +0 -41
  106. package/runtime/sdk/g2/Tileset.ts +0 -71
  107. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  108. package/runtime/sdk/g2/autotile.ts +0 -433
  109. package/runtime/sdk/g2/cells.ts +0 -91
  110. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  111. package/runtime/sdk/g2/groups2d.ts +0 -106
  112. package/runtime/sdk/g2/loop.ts +0 -50
  113. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  114. package/runtime/sdk/g2/touch.ts +0 -83
  115. package/runtime/sdk/gl/Camera.ts +0 -160
  116. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  117. package/runtime/sdk/gl/CharacterController.ts +0 -238
  118. package/runtime/sdk/gl/Gearbox.ts +0 -212
  119. package/runtime/sdk/gl/Geometry.ts +0 -279
  120. package/runtime/sdk/gl/IK.ts +0 -193
  121. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  122. package/runtime/sdk/gl/Light.ts +0 -99
  123. package/runtime/sdk/gl/Lightmap.ts +0 -179
  124. package/runtime/sdk/gl/Material.ts +0 -245
  125. package/runtime/sdk/gl/Mesh.ts +0 -83
  126. package/runtime/sdk/gl/Model.ts +0 -64
  127. package/runtime/sdk/gl/Node.ts +0 -350
  128. package/runtime/sdk/gl/Noise.ts +0 -30
  129. package/runtime/sdk/gl/Particles.ts +0 -676
  130. package/runtime/sdk/gl/Physics.ts +0 -222
  131. package/runtime/sdk/gl/Plane.ts +0 -53
  132. package/runtime/sdk/gl/Ray.ts +0 -16
  133. package/runtime/sdk/gl/Scene.ts +0 -479
  134. package/runtime/sdk/gl/Shape.ts +0 -377
  135. package/runtime/sdk/gl/Texture.ts +0 -46
  136. package/runtime/sdk/gl/Trigger.ts +0 -45
  137. package/runtime/sdk/gl/Vehicle.ts +0 -473
  138. package/runtime/sdk/gl/Wheel.ts +0 -240
  139. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  140. package/runtime/sdk/gl/animation/Animator.ts +0 -87
  141. package/runtime/sdk/gl/animation/Layer.ts +0 -29
  142. package/runtime/sdk/gl/animation/Loop.ts +0 -25
  143. package/runtime/sdk/gl/animation/Playback.ts +0 -43
  144. package/runtime/sdk/gl/animation/core.ts +0 -294
  145. package/runtime/sdk/gl/controls.ts +0 -95
  146. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  147. package/runtime/sdk/gl/scenarios.ts +0 -291
  148. package/runtime/sdk/gl/state.ts +0 -6
  149. package/runtime/sdk/gl/touch.ts +0 -68
  150. package/runtime/sdk/inject.ts +0 -186
  151. package/runtime/sdk/math/Mathf.ts +0 -118
  152. package/runtime/sdk/math/mat4.ts +0 -278
  153. package/runtime/sdk/math/quat.ts +0 -232
  154. package/runtime/sdk/math/vec.ts +0 -255
  155. package/runtime/sdk/plugins/camera.ts +0 -81
  156. package/runtime/sdk/plugins/geolocation.ts +0 -123
  157. package/runtime/sdk/plugins/oauth.ts +0 -61
  158. package/runtime/sdk/plugins/permission.ts +0 -7
  159. package/runtime/sdk/plugins/push.ts +0 -132
  160. package/runtime/sdk/plugins/qr.ts +0 -73
  161. package/runtime/sdk/plugins/service.ts +0 -47
  162. package/runtime/sdk/runtime/app.ts +0 -101
  163. package/runtime/sdk/runtime/appEvents.ts +0 -54
  164. package/runtime/sdk/runtime/channel.ts +0 -61
  165. package/runtime/sdk/runtime/clipboard.ts +0 -20
  166. package/runtime/sdk/runtime/datetime.ts +0 -329
  167. package/runtime/sdk/runtime/device.ts +0 -293
  168. package/runtime/sdk/runtime/fetch.ts +0 -77
  169. package/runtime/sdk/runtime/files.ts +0 -23
  170. package/runtime/sdk/runtime/input.ts +0 -175
  171. package/runtime/sdk/runtime/media.ts +0 -111
  172. package/runtime/sdk/runtime/misc.ts +0 -16
  173. package/runtime/sdk/runtime/net.ts +0 -36
  174. package/runtime/sdk/runtime/rpc.ts +0 -218
  175. package/runtime/sdk/runtime/service.ts +0 -83
  176. package/runtime/sdk/runtime/share.ts +0 -9
  177. package/runtime/sdk/runtime/storage.ts +0 -13
  178. package/runtime/sdk/runtime/touch.ts +0 -76
  179. package/runtime/sdk/scene/defineScene.ts +0 -1227
  180. package/runtime/sdk/scene/editorPlugins.ts +0 -92
  181. package/runtime/sdk/scene/gizmos.ts +0 -148
  182. package/runtime/sdk/scene/grammar.ts +0 -120
  183. package/runtime/sdk/scene/material.ts +0 -188
  184. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  185. package/runtime/sdk/server/auth/global.ts +0 -80
  186. package/runtime/sdk/server/auth/host.ts +0 -318
  187. package/runtime/sdk/server/auth/models.ts +0 -83
  188. package/runtime/sdk/server/auth/types.ts +0 -50
  189. package/runtime/sdk/server/channel.ts +0 -56
  190. package/runtime/sdk/server/context.ts +0 -36
  191. package/runtime/sdk/server/db/defineDb.ts +0 -237
  192. package/runtime/sdk/server/db/fields.ts +0 -132
  193. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  194. package/runtime/sdk/server/db/index.ts +0 -7
  195. package/runtime/sdk/server/db/marci/query.ts +0 -412
  196. package/runtime/sdk/server/db/types.ts +0 -202
  197. package/runtime/sdk/server/errors.ts +0 -12
  198. package/runtime/sdk/server/host.ts +0 -74
  199. package/runtime/sdk/server/inject.ts +0 -13
  200. package/runtime/sdk/server/runtime.ts +0 -133
  201. package/runtime/sdk/server/validate.ts +0 -87
  202. package/runtime/sdk/ui/NativeView.ts +0 -142
  203. package/runtime/sdk/ui/UI.ts +0 -39
  204. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  205. package/runtime/sdk/ui/UIButton.ts +0 -101
  206. package/runtime/sdk/ui/UIContainer.ts +0 -60
  207. package/runtime/sdk/ui/UIImage.ts +0 -83
  208. package/runtime/sdk/ui/UIInput.ts +0 -185
  209. package/runtime/sdk/ui/UIModal.ts +0 -139
  210. package/runtime/sdk/ui/UINode.ts +0 -826
  211. package/runtime/sdk/ui/UIPager.ts +0 -362
  212. package/runtime/sdk/ui/UIPopover.ts +0 -100
  213. package/runtime/sdk/ui/UIScreen.ts +0 -123
  214. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  215. package/runtime/sdk/ui/UISpacer.ts +0 -14
  216. package/runtime/sdk/ui/UITabs.ts +0 -236
  217. package/runtime/sdk/ui/UIText.ts +0 -51
  218. package/runtime/sdk/ui/UIVideo.ts +0 -88
  219. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  220. package/runtime/sdk/ui/UIWidget.ts +0 -127
  221. package/runtime/sdk/ui/fonts.ts +0 -13
  222. package/runtime/sdk/ui/presentable.ts +0 -117
  223. package/runtime/sdk/ui/router.ts +0 -132
  224. package/runtime/sdk/ui/theme.ts +0 -84
@@ -1,512 +0,0 @@
1
- // The Aspect system: attachable capabilities (animation, collider, physics, and user behaviors)
2
- // that hang off a node WITHOUT bloating the node class. Composition with the terseness of methods.
3
- //
4
- // const hero = new Sprite({ texture })
5
- // .aspect(SpriteAnimation, { fps: 8, clips: { walk: [1, 2, 3] } }) // attach + configure
6
- // .aspect(Shape2D, { box: [16, 8] }) // chains: returns the node
7
- // hero.anim.play('walk') // access by name (the chain result is typed as having it)
8
- // if (hero.has(Physics)) hero.physics.velocity = [0, 0] // has() is a type guard
9
- //
10
- // A user aspect declares its name + target node in the generic and reads `this.node`:
11
- // class Health extends Aspect<'health', Sprite> { hp = 100; hurt(n: number) { this.hp -= n } }
12
- // sprite.aspect(Health, { hp: 80 }).health.hurt(10)
13
- //
14
- // SYSTEMS are the same thing attached to a SCENE instead of a node — game logic that has no single
15
- // node to live on (input mapping, FX pools, the HUD, the game mode): `scene.system(Fx)` → `scene.fx`.
16
- // They share the lifecycle, the update phases, the ordering and the events of aspects (see System).
17
- //
18
- // The accessor name lives in the generic (Aspect<'name', Node>). The `chisel` bundler extracts it at
19
- // compile time and injects the runtime `static aspect` on user aspects + the virtual type accessor;
20
- // the SDK's own built-in aspects declare `static readonly aspect` directly (see SpriteAnimation etc.).
21
-
22
- import type { Scene } from "../gl/Scene"
23
- import { Emitter, type EventMap } from "./events"
24
- import type { InspectorUI } from "./InspectorUI"
25
- import type { Node } from "../gl/Node"
26
- import { Time, _registerTimeScaleSink } from "./time"
27
-
28
- // ---- type helpers -----------------------------------------------------------
29
- /** The class object of an aspect — accepted even when its constructor is protected/abstract. */
30
- export type AspectCtor<A extends Aspect<any, any, any>> = abstract new () => A
31
- /** The accessor name carried in the aspect's generic. */
32
- export type KeyOf<A> = A extends Aspect<infer K, any, any> ? K : never
33
- /** The node kind an aspect targets (the scene kind for a System). */
34
- export type TargetOf<A> = A extends Aspect<any, infer P, any> ? P : never
35
- /** `{ name: Aspect }` — the field an aspect contributes to its node. */
36
- export type FieldOf<A extends Aspect<any, any, any>> = { [P in KeyOf<A>]: A }
37
-
38
- type UnionToIntersection<U> =
39
- (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never
40
-
41
- /**
42
- * A variable typed as a node KNOWN to carry the given aspect(s). Union, not a tuple — reads as
43
- * English and mirrors the runtime guard `node.has(Health)`:
44
- * let boss: With<Sprite, Health | Physics>
45
- */
46
- export type With<N, A extends Aspect<any, any, any>> =
47
- N & UnionToIntersection<A extends any ? FieldOf<A> : never>
48
-
49
- // ---- per-frame update dispatch (two phases) ---------------------------------
50
- // Aspects tick every frame in one of two render-synced phases, chosen by WHICH METHOD they define:
51
- // • `update(dt)` — LATE: after the physics step + transform sync + animators, right before the frame
52
- // draws. Reads of node.worldPosition are the FINAL drawn position — cameras/followers land exactly,
53
- // what you write to a plain node is what this frame shows. The default home for game logic.
54
- // • `updateBefore(dt)` — EARLY: before the physics step, so writes of velocity / move commands /
55
- // kinematic transforms are consumed by the SAME frame's step (zero input latency).
56
- // An aspect rarely needs both; when a behaviour does (feed the sim, then pose something after the
57
- // animators), it is two aspects on the same node — not one class in two phases.
58
- //
59
- // The FRAME SOURCE is pluggable so core never names a specific engine bridge (a 3D-only bundle must not
60
- // pull in the 2D one): each engine layer installs a render-synced source via _installAspectFrames (the
61
- // native early/late hooks), so dispatch is in lockstep with the draw. With none installed (a runtime
62
- // without the hooks) it falls back to a single host setLoop, early-then-late — no true post-physics slot.
63
- //
64
- // ORDER within a phase: declared constraints first (`static after = [Controls]` / `static before`),
65
- // then the numeric `updateOrder` (ascending, default 0), then attach order. Constraints are edges between
66
- // classes: an aspect ranks after everything it names in `after` and after everything that names it in
67
- // `before`; a class with no constraints has rank 0. `updateOrder` only breaks ties inside a rank — so a
68
- // declared dependency always wins over a number.
69
- //
70
- // `dt` is GAME time (core/time.ts): scaled by Time.scale, 0 while Time.paused — and a paused frame skips
71
- // every updater except those with `updateWhilePaused = true`. Lazy: the first update-bearing aspect installs
72
- // the dispatch; a project with none starts nothing. No visibility culling yet.
73
- // The dispatcher's view of an instance: the lifecycle members are `protected` on Aspect (the engine calls
74
- // them, game code never does), so core reaches them through this structural type.
75
- interface Updatable {
76
- _phaseSeq: number
77
- _phases: number
78
- updateOrder: number
79
- updateWhilePaused: boolean
80
- update?(dt: number): void
81
- updateBefore?(dt: number): void
82
- onAttach?(): void
83
- onDetach?(): void
84
- onReconfigure?(): void
85
- }
86
- const internals = (a: Aspect<any, any, any>): Updatable => a as unknown as Updatable
87
- type PhaseFn = (dt: number) => void
88
- type Ctor = Function & { after?: Function[], before?: Function[], aspect?: string }
89
-
90
- // A render-synced frame source: given the two phase runners, wire them to the engine and return true;
91
- // return false if unavailable (→ setLoop fallback). Installed by the engine layers (Scene / Scene2D).
92
- type FrameInstaller = (early: PhaseFn, late: PhaseFn) => boolean
93
- let frameInstaller: FrameInstaller | undefined
94
- /** @internal Install a render-synced frame source for aspect update(dt). Called from an engine layer. */
95
- export const _installAspectFrames = (fn: FrameInstaller): void => { frameInstaller = fn }
96
-
97
- let attachSeq = 0
98
- const warned = new Set<string>()
99
- const warnOnce = (msg: string): void => { if (!warned.has(msg)) { warned.add(msg); console.warn(msg) } }
100
-
101
- /** One phase's updater list. Iterated LIVE (no per-frame copy): removals during a pass null the slot
102
- * and compact afterwards; additions land past the pass's end and tick from the next frame. Sorting
103
- * is lazy — re-ranked before the next pass after any add. */
104
- class PhaseList {
105
- readonly items: (Updatable | null)[] = []
106
- private iterating = false
107
- private dirty = false
108
- private holes = false
109
-
110
- add(a: Updatable, phase: "update" | "updateBefore"): void {
111
- a._phaseSeq = ++attachSeq
112
- this.items.push(a)
113
- this.dirty = true
114
- a._phases |= phase === "update" ? 2 : 1
115
- }
116
- remove(a: Updatable): boolean {
117
- const i = this.items.indexOf(a)
118
- if (i < 0) return false
119
- if (this.iterating) { this.items[i] = null; this.holes = true }
120
- else this.items.splice(i, 1)
121
- return true
122
- }
123
- run(method: "update" | "updateBefore", dt: number): void {
124
- if (this.dirty) this.sort()
125
- const items = this.items
126
- const n = items.length // appended during the pass → next frame (attach-order snapshot semantics)
127
- const paused = Time.paused
128
- this.iterating = true
129
- for (let i = 0; i < n; i++) {
130
- const a = items[i]
131
- if (a === null || (paused && !a.updateWhilePaused)) continue
132
- a[method]!(dt)
133
- }
134
- this.iterating = false
135
- if (this.holes) {
136
- this.holes = false
137
- let w = 0
138
- for (let r = 0; r < items.length; r++) { const a = items[r]; if (a !== null) items[w++] = a }
139
- items.length = w
140
- }
141
- }
142
- /** Stable sort by (constraint rank, updateOrder, attach sequence). */
143
- private sort(): void {
144
- this.dirty = false
145
- const ranks = rankConstraints(this.items)
146
- this.items.sort((x, y) => {
147
- if (x === null || y === null) return x === null ? (y === null ? 0 : 1) : -1
148
- const rx = ranks.get(x.constructor) ?? 0, ry = ranks.get(y.constructor) ?? 0
149
- if (rx !== ry) return rx - ry
150
- if (x.updateOrder !== y.updateOrder) return x.updateOrder - y.updateOrder
151
- return x._phaseSeq - y._phaseSeq
152
- })
153
- }
154
- }
155
-
156
- // Constraint rank per class among the classes present in one phase list: rank = 0 with no incoming
157
- // edge, else 1 + max(rank of predecessors). Edges: C after D (from C.after or D.before). A cycle is
158
- // warned once and its closing edge ignored; a constraint naming a class not present in this phase is
159
- // simply unconstrained here (early always precedes late anyway).
160
- const rankConstraints = (items: (Updatable | null)[]): Map<Function, number> => {
161
- const present = new Set<Function>()
162
- for (const a of items) if (a) present.add(a.constructor)
163
- const preds = new Map<Function, Set<Function>>()
164
- const edge = (before: Function, after: Function): void => {
165
- if (!present.has(before) || !present.has(after) || before === after) return
166
- let s = preds.get(after)
167
- if (!s) preds.set(after, (s = new Set()))
168
- s.add(before)
169
- }
170
- for (const c of present) {
171
- for (const d of (c as Ctor).after ?? []) edge(d, c)
172
- for (const d of (c as Ctor).before ?? []) edge(c, d)
173
- }
174
- if (preds.size === 0) return new Map()
175
- const rank = new Map<Function, number>()
176
- const visiting = new Set<Function>()
177
- const rankOf = (c: Function): number => {
178
- const known = rank.get(c)
179
- if (known !== undefined) return known
180
- if (visiting.has(c)) {
181
- warnOnce(`[aspects] ordering cycle through ${c.name || "?"} — its constraint is ignored`)
182
- return 0
183
- }
184
- visiting.add(c)
185
- let r = 0
186
- for (const p of preds.get(c) ?? []) r = Math.max(r, rankOf(p) + 1)
187
- visiting.delete(c)
188
- rank.set(c, r)
189
- return r
190
- }
191
- for (const c of present) rankOf(c)
192
- return rank
193
- }
194
-
195
- const earlyUpdaters = new PhaseList()
196
- const lateUpdaters = new PhaseList()
197
- let dispatchInstalled = false
198
-
199
- const ensureDispatch = (): void => {
200
- if (dispatchInstalled) return
201
- dispatchInstalled = true
202
- // Both phases scale their OWN raw dt (the engine hands the same value to both; a host or test that
203
- // drives them apart still gets each phase's dt right). The frame counter / clocks advance once, early.
204
- const early: PhaseFn = (dt) => { Time._beginFrame(dt); earlyUpdaters.run("updateBefore", Time.dt) }
205
- const late: PhaseFn = (dt) => lateUpdaters.run("update", Time._phaseDt(dt))
206
- // render-synced source if an engine layer installed one; else one host loop (early then late).
207
- if (!(frameInstaller && frameInstaller(early, late))) {
208
- setLoop((dt) => { early(dt); late(dt) })
209
- }
210
- }
211
-
212
- const registerUpdater = (inst: Aspect<any, any, any>): void => {
213
- const a = internals(inst)
214
- const hasBefore = typeof a.updateBefore === "function"
215
- const hasUpdate = typeof a.update === "function"
216
- if (!hasBefore && !hasUpdate) return
217
- if (hasBefore) earlyUpdaters.add(a, "updateBefore")
218
- if (hasUpdate) lateUpdaters.add(a, "update")
219
- ensureDispatch()
220
- }
221
- const unregisterUpdater = (inst: Aspect<any, any, any>): void => {
222
- const a = internals(inst)
223
- if (a._phases === 0) return
224
- earlyUpdaters.remove(a)
225
- lateUpdaters.remove(a)
226
- a._phases = 0
227
- }
228
-
229
- // ---- per-class instance registry (`Aspect.all(Ctor)`) -----------------------
230
- const instances = new Map<Function, Aspect<any, any, any>[]>()
231
- const trackInstance = (a: Aspect<any, any, any>): void => {
232
- let list = instances.get(a.constructor)
233
- if (!list) instances.set(a.constructor, (list = []))
234
- list.push(a)
235
- }
236
- const untrackInstance = (a: Aspect<any, any, any>): void => {
237
- const list = instances.get(a.constructor)
238
- if (!list) return
239
- const i = list.indexOf(a)
240
- if (i >= 0) list.splice(i, 1)
241
- }
242
-
243
- // ---- shared attach / detach (nodes AND scenes) ------------------------------
244
- // `host` receives the named accessor and becomes `inst.node`; `store` is the class → instance map
245
- // (a node's `_aspects`, a scene's `_systems`).
246
- type Store = Map<Function, Aspect<any, any, any>>
247
-
248
- const attachTo = <Self, A extends Aspect<any, any, any>>(
249
- host: Self, store: Store, ctor: AspectCtor<A>, opts: Partial<A> | undefined, scene: unknown,
250
- ): Self & FieldOf<A> => {
251
- let inst = store.get(ctor) as A | undefined
252
- const fresh = inst === undefined
253
- if (!inst) {
254
- inst = new (ctor as unknown as new () => A)()
255
- ;(inst as { node: unknown }).node = host
256
- if (scene !== undefined) (inst as { scene: unknown }).scene = scene
257
- store.set(ctor, inst)
258
- const name = (ctor as Ctor).aspect
259
- if (name) (host as unknown as Record<string, unknown>)[name] = inst
260
- }
261
- if (opts) Object.assign(inst, opts)
262
- if (fresh) {
263
- trackInstance(inst)
264
- internals(inst).onAttach?.()
265
- registerUpdater(inst)
266
- } else if (opts) internals(inst).onReconfigure?.()
267
- return host as Self & FieldOf<A>
268
- }
269
-
270
- const detachFrom = (host: unknown, store: Store, ctor: Function): void => {
271
- const inst = store.get(ctor)
272
- if (!inst) return
273
- internals(inst).onDetach?.()
274
- unregisterUpdater(inst)
275
- untrackInstance(inst)
276
- inst._clearEvents()
277
- store.delete(ctor)
278
- const name = (ctor as Ctor).aspect
279
- if (name) delete (host as Record<string, unknown>)[name]
280
- }
281
-
282
- /** Detach every aspect of a store, last-attached first (a Physics attached after its Shape releases
283
- * its body before the Shape goes). */
284
- const detachAllFrom = (host: unknown, store: Store): void => {
285
- const ctors = [...store.keys()]
286
- for (let i = ctors.length - 1; i >= 0; i--) detachFrom(host, store, ctors[i])
287
- }
288
-
289
- // ---- the host: mixed into every node kind (Node2D, Node) --------------------
290
- /**
291
- * Base for anything that can carry aspects. Extends Emitter, so node kinds get events too. Provides
292
- * the four verbs; the named accessor (node.physics, node.health, …) is set as an own property at
293
- * attach time, keyed by the aspect class's runtime `aspect` name.
294
- */
295
- export abstract class AspectHost<E extends EventMap = EventMap> extends Emitter<E> {
296
- /** @internal class → instance, the authoritative store (named accessors mirror this). */
297
- readonly _aspects = new Map<Function, Aspect<any, any, any>>()
298
-
299
- /** Attach (and configure) an aspect, or reconfigure it if already present. Returns the node typed
300
- * as now-having that aspect (so no guard/`?.` is needed afterwards). Rejects a wrong target node. */
301
- aspect<Self extends TargetOf<A>, A extends Aspect<any, any, any>>(
302
- this: Self,
303
- ctor: AspectCtor<A>,
304
- opts?: Partial<A>,
305
- ): Self & FieldOf<A> {
306
- return attachTo(this, (this as unknown as AspectHost)._aspects, ctor, opts, undefined)
307
- }
308
-
309
- /** Safe access — undefined if the aspect isn't attached. */
310
- get<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): A | undefined {
311
- return this._aspects.get(ctor) as A | undefined
312
- }
313
-
314
- /** Existence check AND type guard: inside `if (node.has(Physics))`, `node.physics` is present. */
315
- has<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): this is this & FieldOf<A> {
316
- return this._aspects.has(ctor)
317
- }
318
-
319
- /** Detach an aspect (runs its onDetach). Named `removeAspect` so it never collides with the
320
- * child-management `add`/`remove` semantics some node kinds expose. */
321
- removeAspect<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): this {
322
- detachFrom(this, this._aspects, ctor)
323
- return this
324
- }
325
-
326
- /** @internal Detach every aspect (node teardown): onDetach, updaters, events, registries. */
327
- _detachAll(): void {
328
- detachAllFrom(this, this._aspects)
329
- this.clearListeners()
330
- }
331
- }
332
-
333
- // ---- systems: aspects of a scene --------------------------------------------
334
- /** @internal What a scene needs to carry systems. Scene / Scene2D implement it over these helpers. */
335
- export type SystemHost = { readonly _systems: Map<Function, Aspect<any, any, any>> }
336
-
337
- /** @internal `scene.system(Ctor, opts)`. */
338
- export const _attachSystem = <Self extends SystemHost, A extends Aspect<any, any, any>>(
339
- scene: Self, ctor: AspectCtor<A>, opts?: Partial<A>,
340
- ): Self & FieldOf<A> => attachTo(scene, scene._systems, ctor, opts, scene)
341
- /** @internal `scene.removeSystem(Ctor)`. */
342
- export const _detachSystem = (scene: SystemHost, ctor: Function): void => detachFrom(scene, scene._systems, ctor)
343
- /** @internal Scene teardown: every system, last-attached first. */
344
- export const _detachAllSystems = (scene: SystemHost): void => detachAllFrom(scene, scene._systems)
345
-
346
- /** @internal Engine layers call this once with their bridge's clock hook (feature-detected). */
347
- export const _installTimeScale = (sink: (scale: number) => void): void => _registerTimeScaleSink(sink)
348
-
349
- // ---- the aspect base --------------------------------------------------------
350
- /**
351
- * Base for everything you attach. `K` = accessor name (extracted by chisel for the runtime). `P` =
352
- * the target node kind: `this.node` is typed to it, AND attaching to a wrong node is a compile error.
353
- * `E` = the events this aspect emits (`{ explode: (b: Barrel) => void }`): `this.emit('explode', …)`
354
- * inside, `node.barrel.on('explode', fn)` outside.
355
- * Initialize in `onAttach` (the node is set by then) — not a constructor; aspects are created by the
356
- * engine via `node.aspect()`, never `new`.
357
- */
358
- export abstract class Aspect<K extends string, P = Node, E extends EventMap = {}> {
359
- /**
360
- * Opt this aspect class into EDITOR-RUN mode (generators): while a scene is edited, the scene
361
- * loader constructs the instance (refs resolved, `node`/`generated` set — never `onAttach`) and
362
- * calls `rebuild()` — again on inspector prop edits and when a `ref()`-referenced node moves.
363
- * `static editor = { rebuild: true }`.
364
- */
365
- static editor?: { rebuild?: boolean }
366
-
367
- /**
368
- * Custom inspector card for this aspect in the scene editor (immediate-mode — see
369
- * core/InspectorUI.ts): re-runs on every edit/event and emits a widget list. Without it, the
370
- * editor shows the inferred fields; `ui.auto()` emits those same fields, so a custom inspector
371
- * usually starts with `ui.auto()` and appends status lines / buttons / dynamic dropdowns:
372
- *
373
- * static inspector(ui: InspectorUI, self: MyAspect) {
374
- * ui.auto()
375
- * if (ui.button('Randomize')) self.rebuild()
376
- * }
377
- */
378
- static inspector?: (ui: InspectorUI, aspect: any) => void
379
-
380
- /** Tick AFTER these classes within the same phase (`static after = [Controls]`). A constraint
381
- * always beats the numeric `updateOrder`. Classes absent from the phase are ignored. */
382
- static after?: Function[]
383
- /** Tick BEFORE these classes within the same phase (`static before = [Camera]`). */
384
- static before?: Function[]
385
-
386
- /** Every live instance of an aspect/system class, in attach order — the registry a game used to
387
- * hand-roll as `static all[]`. Live, read-only: copy it (`[...Aspect.all(Barrel)]`) before a loop
388
- * that detaches. Empty in the scene editor (aspects are inert data there). */
389
- static all<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): readonly A[] {
390
- return (instances.get(ctor) ?? []) as unknown as readonly A[]
391
- }
392
-
393
- // NOTE: P has no `extends AspectHost` constraint on purpose — Emitter<E>'s generic
394
- // addEventListener makes a specific-event node (AspectHost<NodeEvents>) not assignable to
395
- // AspectHost<any>, which would reject every real node. P only types `this.node`; target
396
- // enforcement happens in aspect() via `this: Self extends TargetOf<A>`. Defaults to Node
397
- // (3D is the default engine) — a 2D aspect declares its own P, e.g. Aspect<'x', Sprite>.
398
- /** The node this aspect is attached to (set at attach time). For a System: the scene. */
399
- readonly node!: P
400
- /**
401
- * GENERATOR aspects (`static editor = { rebuild: true }`, attached through a scene file): a
402
- * scene-added container child for the aspect's generated output — `rebuild()` clears and refills
403
- * it. Provided by the scene loader before `onAttach`/`rebuild` run; nodes `add()`ed to it join
404
- * the scene's draw set automatically (membership is separate from parenting). Undefined for
405
- * aspects attached by hand outside scene files. See docs/3d/scene-files.md.
406
- */
407
- readonly generated!: Node & { clear(): void, readonly version: number }
408
- /**
409
- * GENERATOR aspects attached through a scene file: the `Scene` the host node was built into —
410
- * what `handle.instantiate({ scene })` / `scene.add()` need inside `rebuild()`. Set by the scene
411
- * loader (edit AND play mode); undefined for aspects attached by hand. 3D only (2D aspects own
412
- * their `scene` field). For a System: always set — the scene it is attached to.
413
- */
414
- readonly scene!: P extends Node ? Scene : P extends SystemHost ? P : unknown
415
- // Phantom fields — they let the type system recover `K` (the accessor name) and `E` from an
416
- // instance type (`KeyOf<A>`), never read at runtime. Deliberately NOT marked as internal-only:
417
- // build-types emits with `stripInternal` (which matches that tag in ANY leading comment, `//`
418
- // included), and without these in the public .d.ts `infer K` collapses to `string`, which turned
419
- // every `node.<accessor>` / `scene.<accessor>` into an untyped index signature in projects.
420
- declare protected readonly __key__?: K
421
- declare protected readonly __events__?: E
422
- /** @internal attach sequence within the phase lists (tie-breaker). */
423
- _phaseSeq = 0
424
- /** @internal bit 1 = registered early, bit 2 = registered late. */
425
- _phases = 0
426
- private _listeners?: Map<string, Function[]>
427
-
428
- // ---- lifecycle: the engine calls these, game code never does (hence `protected`; a subclass may
429
- // still widen one to public when a method doubles as an explicit action). ----
430
- /** The node (or scene) is set — initialize here, not in a constructor. */
431
- protected onAttach?(): void
432
- /** Release what onAttach acquired. */
433
- protected onDetach?(): void
434
- /** Called after `node.aspect(Ctor, opts)` re-assigns options on an ALREADY attached aspect (a
435
- * pre-attached one like `model.anim`): rebuild whatever was derived from the options at attach. */
436
- protected onReconfigure?(): void
437
- /**
438
- * GENERATOR aspects: (re)build the derived output under `this.generated` — must be idempotent
439
- * (clear, then create). Call it from `onAttach()` for play mode; a class opting in with
440
- * `static editor = { rebuild: true }` ALSO runs it while a scene is being edited: once at load,
441
- * and again whenever an inspector prop changes or a `ref()`-referenced node moves.
442
- */
443
- rebuild?(): void
444
- /**
445
- * LATE phase — every frame while attached, after the physics step + transform sync + animators,
446
- * right before the frame draws: reads of `node.worldPosition` are the final drawn position
447
- * (cameras/followers have no 1-frame lag) and what you write to a plain node is what this frame
448
- * shows. The default home for game logic. `dt` = GAME seconds since the last frame (`Time.scale`
449
- * applied; 0 while paused — see `updateWhilePaused`).
450
- */
451
- protected update?(dt: number): void
452
- /**
453
- * EARLY phase — every frame BEFORE the physics step, so what you feed the simulation (velocity,
454
- * `controller.move()`, forces, kinematic transforms) is consumed by this same frame's step: zero
455
- * input latency. Use it only when you FEED the simulation. Reads here see last frame's settled
456
- * state. A class rarely needs both phases — that is two aspects on one node.
457
- */
458
- protected updateBefore?(dt: number): void
459
-
460
- /** Tick order within a phase and rank — ascending; default 0, ties keep attach order. A declared
461
- * `static after` / `static before` constraint always beats this number. Read once, at attach —
462
- * set it as a class field. */
463
- protected updateOrder = 0
464
- /** Keep ticking while `Time.paused` (a HUD fade, the pause menu). `dt` is still 0 then — read
465
- * `Time.unscaledDt` for wall-clock motion. Checked every frame; may be toggled at any time. */
466
- protected updateWhilePaused = false
467
-
468
- /** Opt-in: only run update(dt) while the node is on-screen. NOOP for now — visibility culling isn't
469
- * wired yet, so every updater ticks regardless; declared so aspects can opt in ahead of it. */
470
- protected updateWhenVisible = false
471
-
472
- // ---- events: what this aspect tells the world (`E` in the generic) ----
473
- /** Listen to one of this aspect's events. Chainable. Cleared on detach. */
474
- on<C extends keyof E & string>(channel: C, callback: E[C]): this {
475
- const map = (this._listeners ??= new Map())
476
- const list = map.get(channel)
477
- if (list) list.push(callback)
478
- else map.set(channel, [callback])
479
- return this
480
- }
481
- /** Remove a listener added with `on` (same function reference). */
482
- off<C extends keyof E & string>(channel: C, callback: E[C]): this {
483
- const list = this._listeners?.get(channel)
484
- if (list) { const i = list.indexOf(callback); if (i >= 0) list.splice(i, 1) }
485
- return this
486
- }
487
- /** Fire one of this aspect's events. Protected: only the aspect itself emits. */
488
- protected emit<C extends keyof E & string>(channel: C, ...args: Parameters<E[C]>): void {
489
- const list = this._listeners?.get(channel)
490
- if (!list || list.length === 0) return
491
- for (const fn of list.slice()) fn(...args)
492
- }
493
- /** @internal */
494
- _clearEvents(): void { this._listeners = undefined }
495
- }
496
-
497
- /**
498
- * An aspect of a SCENE: game logic with no single node to live on — input mapping, the FX pools, the
499
- * HUD, the game mode. Same lifecycle (`onAttach`/`onDetach`), same two phases (`updateBefore` /
500
- * `update`), same ordering (`updateOrder`, `static after`) and events as a node aspect; `this.scene` is the
501
- * scene it was attached to. `S` = the scene kind (`Scene` by default, `Scene2D` for 2D games).
502
- *
503
- * class Hud extends System<'hud'> {
504
- * static after = [Player]
505
- * update(dt: number) { … }
506
- * }
507
- * scene.system(Hud) // attach
508
- * scene.hud // the typed accessor
509
- */
510
- export abstract class System<K extends string, S extends SystemHost = Scene, E extends EventMap = {}> extends Aspect<K, S, E> {
511
- // `this.scene` (and `this.node`) are typed S by the base class's conditional — nothing to add.
512
- }