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,277 +0,0 @@
1
- // Fine-grained reactivity for the retained-mode UI: signal() / computed() / effect() globals plus
2
- // the element-binding helpers UINode/UIText use for function-valued style props and text.
3
- //
4
- // Design notes (the parts that are deliberate, not incidental):
5
- // - Dependents (effects, computeds) hold their sources STRONGLY; sources hold dependents WEAKLY
6
- // (a WeakRef in the subscriber set). An element owns its binding effects, the effect's closure
7
- // owns the element — so when user code drops a subtree, the whole element+effect subgraph
8
- // becomes unreachable and collects, and the signal prunes the dead WeakRef on its next write.
9
- // No unmount hook is needed and re-appending a kept subtree keeps its bindings alive.
10
- // - Free-standing effect() computations are additionally held in a strong root set (otherwise a
11
- // `effect(() => ...)` whose handle the user discards would be GC'd mid-flight); dispose()
12
- // releases them. Element bindings are NOT in the root set — the element is their owner.
13
- // - Writes are batched: a signal write schedules a microtask flush; each dirty effect runs once
14
- // per flush. The first run of any effect/binding is synchronous, so element styles hold
15
- // concrete values immediately after construction (hosts serialize _style at mount).
16
- // - QuickJS-safe: promise microtasks (the hosts drain pending jobs), WeakRef with a strong-ref
17
- // fallback for engines without it (then stale subscribers just live until the run ends —
18
- // every run executes in a fresh JSRuntime anyway).
19
-
20
- interface WeakRefLike<T extends object> { deref(): T | undefined }
21
-
22
- const WeakRefImpl: new <T extends object>(target: T) => WeakRefLike<T> =
23
- typeof WeakRef !== "undefined" ? WeakRef : class StrongRef<T extends object> {
24
- private _t: T
25
- constructor(target: T) { this._t = target }
26
- deref(): T | undefined { return this._t }
27
- }
28
-
29
- interface Computation {
30
- _ref: WeakRefLike<Computation>
31
- _deps: Dep[]
32
- _disposed: boolean
33
- _invalidate(): void
34
- }
35
-
36
- let activeComputation: Computation | null = null
37
-
38
- /** Removes `c` from every source it subscribed to on its last run. */
39
- const cleanupDeps = (c: Computation) => {
40
- for (const dep of c._deps) dep._subs.delete(c._ref)
41
- c._deps.length = 0
42
- }
43
-
44
- class Dep {
45
- _subs = new Set<WeakRefLike<Computation>>()
46
-
47
- _track() {
48
- const c = activeComputation
49
- if (c === null || this._subs.has(c._ref)) return
50
- this._subs.add(c._ref)
51
- c._deps.push(this)
52
- }
53
-
54
- _notify() {
55
- for (const ref of [ ...this._subs ]) {
56
- const c = ref.deref()
57
- if (c === undefined || c._disposed) {
58
- this._subs.delete(ref) // lazy pruning of collected/disposed subscribers
59
- } else {
60
- c._invalidate()
61
- }
62
- }
63
- }
64
- }
65
-
66
- // ---- effect scheduling (microtask batch) ----
67
-
68
- const queue = new Set<EffectImpl>()
69
- let flushScheduled = false
70
-
71
- const flush = () => {
72
- // Effects may write signals, re-filling the queue mid-flush; drain until empty with a guard
73
- // against write-loops (effect A invalidating itself / a cycle of effects).
74
- let runs = 0
75
- while (queue.size > 0) {
76
- if (++runs > 10000) {
77
- queue.clear()
78
- console.warn("signals: effect flush exceeded 10000 runs — possible infinite update loop, remaining effects dropped")
79
- break
80
- }
81
- const first: EffectImpl = queue.values().next().value!
82
- queue.delete(first)
83
- first._run()
84
- }
85
- flushScheduled = false
86
- }
87
-
88
- const schedule = (fx: EffectImpl) => {
89
- queue.add(fx)
90
- if (!flushScheduled) {
91
- flushScheduled = true
92
- Promise.resolve().then(flush)
93
- }
94
- }
95
-
96
- /** Runs every pending effect now instead of waiting for the microtask (test/host hook). */
97
- export const flushEffects = (): void => {
98
- if (queue.size > 0) flush()
99
- }
100
-
101
- class EffectImpl implements Computation {
102
- _ref: WeakRefLike<Computation>
103
- _deps: Dep[] = []
104
- _disposed = false
105
- private _fn: () => void
106
-
107
- constructor(fn: () => void) {
108
- this._fn = fn
109
- this._ref = new WeakRefImpl(this)
110
- }
111
-
112
- _run() {
113
- if (this._disposed) return
114
- cleanupDeps(this)
115
- const prev = activeComputation
116
- activeComputation = this
117
- try {
118
- this._fn()
119
- } finally {
120
- activeComputation = prev
121
- }
122
- }
123
-
124
- _invalidate() {
125
- schedule(this)
126
- }
127
-
128
- _dispose() {
129
- if (this._disposed) return
130
- this._disposed = true
131
- cleanupDeps(this)
132
- queue.delete(this)
133
- rootEffects.delete(this)
134
- }
135
- }
136
-
137
- // Free-standing effect() handles the user may never store — kept alive here until disposed.
138
- const rootEffects = new Set<EffectImpl>()
139
-
140
- // ---- public surface ----
141
-
142
- export interface Signal<T> {
143
- value: T,
144
- /** Read without subscribing the current effect/computed. */
145
- peek(): T,
146
- }
147
-
148
- export interface Computed<T> {
149
- readonly value: T,
150
- /** Read without subscribing the current effect/computed (still recomputes if stale). */
151
- peek(): T,
152
- }
153
-
154
- class SignalImpl<T> {
155
- private _dep = new Dep()
156
- private _v: T
157
-
158
- constructor(value: T) {
159
- this._v = value
160
- }
161
-
162
- get value(): T {
163
- this._dep._track()
164
- return this._v
165
- }
166
-
167
- set value(next: T) {
168
- if (Object.is(next, this._v)) return
169
- this._v = next
170
- this._dep._notify()
171
- }
172
-
173
- peek(): T {
174
- return this._v
175
- }
176
- }
177
-
178
- class ComputedImpl<T> implements Computation {
179
- _ref: WeakRefLike<Computation>
180
- _deps: Dep[] = []
181
- _disposed = false
182
- private _dep = new Dep()
183
- private _fn: () => T
184
- private _stale = true
185
- private _v!: T
186
-
187
- constructor(fn: () => T) {
188
- this._fn = fn
189
- this._ref = new WeakRefImpl(this)
190
- }
191
-
192
- get value(): T {
193
- this._dep._track()
194
- if (this._stale) this._recompute()
195
- return this._v
196
- }
197
-
198
- peek(): T {
199
- if (this._stale) this._recompute()
200
- return this._v
201
- }
202
-
203
- private _recompute() {
204
- cleanupDeps(this)
205
- const prev = activeComputation
206
- activeComputation = this
207
- try {
208
- this._v = this._fn()
209
- this._stale = false
210
- } finally {
211
- activeComputation = prev
212
- }
213
- }
214
-
215
- // Lazy: a dep change only marks the memo stale and passes the wave downstream; the next read
216
- // recomputes.
217
- _invalidate() {
218
- if (this._stale) return
219
- this._stale = true
220
- this._dep._notify()
221
- }
222
- }
223
-
224
- /**
225
- * A reactive value. Reading `.value` inside an `effect`/`computed` (or a function-valued style
226
- * prop / `UIText` content) subscribes it; writing `.value` re-runs subscribers (batched — once
227
- * per microtask).
228
- */
229
- export function signal<T>(initialValue: T): Signal<T>
230
- export function signal<T = undefined>(): Signal<T | undefined>
231
- export function signal<T>(initialValue?: T): Signal<T | undefined> {
232
- return new SignalImpl(initialValue)
233
- }
234
-
235
- /** A lazily-cached derived value: recomputes on read after any of its dependencies changed. */
236
- export function computed<T>(fn: () => T): Computed<T> {
237
- return new ComputedImpl(fn)
238
- }
239
-
240
- /**
241
- * Runs `fn` now and again whenever any signal/computed it read changes (batched per microtask).
242
- * Returns a dispose function; an undisposed effect lives for the rest of the app run.
243
- */
244
- export function effect(fn: () => void): () => void {
245
- const fx = new EffectImpl(fn)
246
- rootEffects.add(fx)
247
- fx._run()
248
- return () => fx._dispose()
249
- }
250
-
251
- // ---- element bindings (internal — used by UINode/UIText for function-valued props) ----
252
-
253
- /** The shape UINode's Element exposes for binding ownership. */
254
- export interface BindingHost {
255
- /** @internal */ _fx?: Map<string, EffectImpl>
256
- }
257
-
258
- /**
259
- * @internal Creates (or replaces) the reactive binding stored under `key` on the element. The
260
- * effect is owned by the element (strong ref in `_fx`), NOT by the root set — discard the element
261
- * and the binding collects with it. Runs synchronously once.
262
- */
263
- export function createBinding(el: BindingHost, key: string, run: () => void): void {
264
- const map = el._fx ?? (el._fx = new Map())
265
- map.get(key)?._dispose()
266
- const fx = new EffectImpl(run)
267
- map.set(key, fx)
268
- fx._run()
269
- }
270
-
271
- /** @internal Disposes the binding stored under `key`, if any (static value overriding a binding). */
272
- export function disposeBinding(el: BindingHost, key: string): void {
273
- const fx = el._fx?.get(key)
274
- if (fx === undefined) return
275
- fx._dispose()
276
- el._fx!.delete(key)
277
- }
@@ -1,81 +0,0 @@
1
- // The game clock: one place that owns "how fast does the game run" — `Time.scale` (slow-motion, 0.5),
2
- // `Time.paused` (a menu over a frozen world) — and what a frame is worth (`Time.dt`).
3
- //
4
- // The aspect dispatcher (core/Aspect.ts) calls `_beginFrame(rawDt)` once per frame with the host's
5
- // wall-clock delta; `update*(dt)` callbacks then receive the SCALED delta (0 while paused). The same
6
- // scale is pushed to every engine that registered a sink (creator-gl / creator-2d `setTimeScale`),
7
- // so physics, animators and particles slow down and freeze together with the game code — a paused
8
- // aspect and a paused rigid body agree. `Time.unscaledDt` is the wall-clock delta for things that
9
- // must keep moving while the game is paused (a HUD fade, a pause-menu animation): those aspects set
10
- // `updateWhilePaused = true` and read it themselves, exactly like Unity's unscaledDeltaTime.
11
-
12
- type ScaleSink = (scale: number) => void
13
- const sinks: ScaleSink[] = []
14
-
15
- class TimeClock {
16
- private _scale = 1
17
- private _paused = false
18
- /** Seconds of GAME time since the app started (scaled; stops while paused). */
19
- now = 0
20
- /** Seconds of wall-clock time since the app started (never stops). */
21
- unscaledNow = 0
22
- /** This frame's delta in game seconds — what `update(dt)` receives. 0 while paused. */
23
- dt = 0
24
- /** This frame's wall-clock delta in seconds (the host's frame time, clamped by the engine). */
25
- unscaledDt = 0
26
- /** Frames dispatched since the app started (counts paused frames too). */
27
- frame = 0
28
-
29
- /** Speed of the game relative to wall-clock: 1 normal, 0.5 half speed, 2 double. Applied to
30
- * `dt`, `now` AND the engines' physics / animation / particle clocks. Negative or NaN → 0. */
31
- get scale(): number { return this._scale }
32
- set scale(v: number) {
33
- const s = Number.isFinite(v) && v > 0 ? v : 0
34
- if (s === this._scale) return
35
- this._scale = s
36
- this._push()
37
- }
38
-
39
- /** Freeze the game: `dt` reads 0, aspects without `updateWhilePaused` are skipped, the engines'
40
- * simulations stop. `scale` is remembered and restored on resume. */
41
- get paused(): boolean { return this._paused }
42
- set paused(v: boolean) {
43
- if (v === this._paused) return
44
- this._paused = v
45
- this._push()
46
- }
47
-
48
- /** The scale the engines currently run at (0 while paused). */
49
- get effectiveScale(): number { return this._paused ? 0 : this._scale }
50
-
51
- private _push(): void {
52
- const s = this.effectiveScale
53
- for (const sink of sinks) sink(s)
54
- }
55
-
56
- /** @internal Called by the aspect dispatcher at the top of every frame with the host's raw delta. */
57
- _beginFrame(rawDt: number): void {
58
- const raw = Number.isFinite(rawDt) && rawDt > 0 ? rawDt : 0
59
- this.frame++
60
- this.unscaledNow += raw
61
- this.now += this._phaseDt(raw)
62
- }
63
- /** @internal A phase's scaled delta from its raw one; records both as this frame's `dt`. */
64
- _phaseDt(rawDt: number): number {
65
- const raw = Number.isFinite(rawDt) && rawDt > 0 ? rawDt : 0
66
- this.unscaledDt = raw
67
- this.dt = raw * this.effectiveScale
68
- return this.dt
69
- }
70
- }
71
-
72
- /** The game clock — scale / pause the whole game, read `dt` / `now` anywhere. */
73
- export const Time = new TimeClock()
74
-
75
- /** @internal Engine layers (Scene / Scene2D) register the bridge call that forwards the effective
76
- * scale to their native clocks. Pushed immediately with the current value so a scene created after
77
- * `Time.paused = true` starts frozen. */
78
- export const _registerTimeScaleSink = (sink: ScaleSink): void => {
79
- sinks.push(sink)
80
- sink(Time.effectiveScale)
81
- }
@@ -1,40 +0,0 @@
1
- // Orthographic camera for a Scene2D. Position / zoom / rotation live natively. To make the camera
2
- // track a node, attach a small update(dt) aspect that eases camera.position toward the node — there's
3
- // no built-in follow() (and so nothing to dispose).
4
-
5
- import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
6
- import type { CompAxis, CompWriter } from "../core/compWrite"
7
-
8
- export class Camera2D implements CompWriter {
9
- private _x = 0
10
- private _y = 0
11
- private _zoom = 1
12
- private _rotation = 0
13
-
14
- private readonly sceneId: number
15
- constructor(sceneId: number) { this.sceneId = sceneId }
16
-
17
- get position(): Vec2 { return new Vec2(this._x, this._y) }
18
- set position(v: Vec2Like) { this._x = cx(v); this._y = cy(v); _creator2d.cameraSetPosition(this.sceneId, this._x, this._y) }
19
- /** @internal chisel `comp_write` (see core/compWrite.ts): `camera.position.<axis> = v` compiles to a call here. */
20
- static _comps = [ "position" ]
21
- _writeComp(_prop: string, axis: CompAxis, v: number): void {
22
- if (axis === "x") this.position = [ v, this._y ]
23
- else if (axis === "y") this.position = [ this._x, v ]
24
- }
25
-
26
- get zoom(): number { return this._zoom }
27
- set zoom(z: number) { this._zoom = z; _creator2d.cameraSetZoom(this.sceneId, z) }
28
-
29
- get rotation(): number { return this._rotation }
30
- set rotation(deg: number) { this._rotation = deg; _creator2d.cameraSetRotation(this.sceneId, deg) }
31
-
32
- screenToWorld(screenX: number, screenY: number): Vec2 {
33
- const o = _creator2d.cameraScreenToWorld(this.sceneId, screenX, screenY)
34
- return new Vec2(o[0], o[1])
35
- }
36
- worldToScreen(worldX: number, worldY: number): Vec2 {
37
- const o = _creator2d.cameraWorldToScreen(this.sceneId, worldX, worldY)
38
- return new Vec2(o[0], o[1])
39
- }
40
- }
@@ -1,276 +0,0 @@
1
- // A character controller, as an aspect on a Node2D, backed by Box2D's kinematic MOVER — collide and
2
- // slide, not a rigid body. Requires a capsule Shape2D (one is derived from the sprite if absent) and
3
- // does NOT use Physics2D.
4
- //
5
- // const hero = new Sprite({ texture, anchor: [0.5, 1] })
6
- // .aspect(Shape2D, { capsule: { from: [0, 8], to: [0, 26], radius: 8 } })
7
- // .aspect(CharacterController2D)
8
- //
9
- // setLoop(() => hero.controller.move(((Input.key('KeyD') ? 1 : 0) - (Input.key('KeyA') ? 1 : 0)) * SPEED))
10
- // Input.on('keydown', e => { if (e.code === 'Space' && hero.controller.grounded) hero.controller.velocityY = 700 })
11
- //
12
- // The two halves of the velocity have different NATURES, so they have different channels:
13
- //
14
- // • HORIZONTAL — `move(x)`, world units/s, a PER-FRAME COMMAND. A walking character's horizontal
15
- // velocity is muscle-driven and not conserved: stop pushing, stop moving. So the command expires
16
- // each frame — no call this frame means standing still, and releasing the keys needs no explicit
17
- // zero. It is latched for the whole frame, so every physics sub-step of that frame sees it.
18
- // • VERTICAL — `velocityY`, LATCHED ballistic state. Gravity integrates into it in the engine; you
19
- // seed it for a jump / bounce pad / explosion. There is deliberately no ground check: coyote
20
- // time, double jumps and wall jumps all need the caller's own condition.
21
- //
22
- // FREE MODE — `gravityScale = 0` turns the character into a swimmer / flyer / ladder climber / the
23
- // hero of a TOP-DOWN game: no gravity, no ground, no stick-to-floor. There the vertical is
24
- // muscle-driven too, so `move()` takes BOTH components and the whole velocity becomes a per-frame
25
- // command. Top-down vs platformer is this one number, not two controllers.
26
- //
27
- // `velocity` reads back what the solver ENDED UP with after the last step (post-collision) — walk into
28
- // a wall and it reads ~0 even though you commanded 300. Everything runs on the engine's fixed clock,
29
- // so the aspect has no per-frame update: a character costs zero JS work per frame.
30
- //
31
- // ROTATION AND SCALE stay the node's: the engine writes only the position (a capsule is symmetric
32
- // about its axis, so its angle is physically meaningless).
33
- //
34
- // The character IS visible to raycasts, overlaps and triggers — it carries a hidden kinematic body for
35
- // exactly that, so pickups and hazard zones need no special code. Solid contacts do not arrive as
36
- // 'enter' events (the mover is not a body); read `collisions` for those.
37
-
38
- import { Aspect } from "../core/Aspect"
39
- import type { FieldMeta } from "../core/fields"
40
- import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
41
- import type { CompAxis, CompWriter } from "../core/compWrite"
42
- import { PhysicsGroup2D, _watchGroupChanges, _unwatchGroupChanges, categoryOf, solidMask } from "./groups2d"
43
- import { Shape2D } from "./Shape2D"
44
- import { node2dRegistry, type Node2D } from "./Node2D"
45
-
46
- /** Where the character's feet are, as reported by the solver after the last step. */
47
- export type GroundState2D = "ground" | "slope" | "air"
48
- const GROUND_STATES: readonly GroundState2D[] = [ "ground", "slope", "air" ]
49
-
50
- /** One surface the mover pushed out of this step. `normal` points back at the character. */
51
- export type Collision2D = { node: Node2D | null, normal: Vec2 }
52
-
53
- const MAX_COLLISIONS = 8
54
- const state = new Float32Array(8)
55
- let warnedFreeMove = false
56
-
57
- export class CharacterController2D extends Aspect<"controller", Node2D> implements CompWriter {
58
- static readonly aspect = "controller"
59
-
60
- // Accessor-backed tunables are invisible to describeFields (it enumerates own enumerable fields),
61
- // so the inspector is told about them explicitly. Defaults still come from the getters.
62
- static fields: FieldMeta<CharacterController2D> = {
63
- gravityScale: { editor: "number", min: 0, step: 0.1, label: "Gravity scale" },
64
- maxSlope: { editor: "number", min: 0, max: 89, step: 1, label: "Max slope°" },
65
- }
66
-
67
- private _id = 0
68
- private _gravityScale = 1
69
- private _maxSlope = 45
70
- private _group: PhysicsGroup2D | undefined
71
- private _dropThrough: Node2D | null = null
72
- private _repush = (): void => {
73
- if (this._id) _creator2d.characterSetFilter(this._id, categoryOf(this._group), solidMask(this._group))
74
- }
75
-
76
- onAttach(): void {
77
- if (!_creator2d.physicsHasSupport || !_creator2d.physicsHasSupport()) return
78
- if (!this.node.has(Shape2D)) this.node.aspect(Shape2D, {})
79
- const shape = this.node.get(Shape2D)!
80
- const cap = shape._capsule()
81
- // Read the options through their PUBLIC accessors — chisel drops an accessor pair whose name is
82
- // only ever an object-literal key, and the option would silently do nothing (see Physics2D).
83
- this._id = _creator2d.characterCreate(this.node.id, cap.x1, cap.y1, cap.x2, cap.y2, cap.radius,
84
- this.maxSlope, categoryOf(this.group), solidMask(this.group))
85
- if (!this._id) return
86
- shape._claimedByCharacter()
87
- _creator2d.characterSetGravityScale(this._id, this.gravityScale)
88
- this.node._xf = this._id // routes node.position writes to the character
89
- this.node._xfKind = 1
90
- // The engine moves this node every step, so its JS transform cache must re-sync on READ — without
91
- // this the character simulates correctly and node.position never changes.
92
- this.node._physicsDriven = true
93
- if (this._group) _watchGroupChanges(this._repush)
94
- }
95
-
96
- onDetach(): void {
97
- _unwatchGroupChanges(this._repush)
98
- if (this._id) { _creator2d.characterDestroy(this._id); this._id = 0 }
99
- this.node._xf = 0
100
- this.node._xfKind = 0
101
- this.node._physicsDriven = false
102
- }
103
-
104
- /** Native character id (0 until attached / no physics support). */
105
- get id(): number { return this._id }
106
-
107
- /**
108
- * This frame's movement command, in world units/s — NOT normalized, NOT a per-frame displacement
109
- * (that is what Unity's `Move` takes; passing `v * dt` here gives a character 60× too slow).
110
- *
111
- * ONE component = horizontal, the everyday platformer call. TWO = the whole velocity, for free mode
112
- * (`gravityScale = 0`); with gravity on, a two-component call fights the ballistic vertical and the
113
- * character hangs in the air, so that combination warns once.
114
- *
115
- * Sticky only within the frame: the command expires once the engine consumes it. It also **takes the
116
- * axis back from a latched `velocity`** — commanding is claiming ownership, which is what keeps the
117
- * two horizontal sources from ever fighting.
118
- */
119
- move(x: number): void
120
- move(x: number, y: number): void
121
- move(v: Vec2Like): void
122
- move(x: number | Vec2Like, y?: number): void {
123
- if (!this._id) return
124
- if (typeof x === "number") {
125
- if (y === undefined) { _creator2d.characterMove(this._id, x); return }
126
- this._warnFree()
127
- _creator2d.characterMoveFree(this._id, x, y)
128
- return
129
- }
130
- this._warnFree()
131
- _creator2d.characterMoveFree(this._id, cx(x), cy(x))
132
- }
133
-
134
- private _warnFree(): void {
135
- if (this._gravityScale === 0 || warnedFreeMove) return
136
- warnedFreeMove = true
137
- console.warn("CharacterController2D.move(): the two-component form is the FREE-mode command (top-down / swimming / flying) and expects gravityScale = 0. With gravity on it overwrites the falling speed every frame, so the character will hang in the air. Use move(x) and velocityY for a platformer.")
138
- }
139
-
140
- /** Vertical velocity (world units/s) — LATCHED: gravity works on it, you seed it. `= 700` to jump,
141
- * `+= 300` to stack an explosion on top of the current motion. No ground check: guard it yourself
142
- * with `grounded` (or don't, for a double jump). Does nothing in free mode. `velocity.y = 700` is
143
- * the same channel — pick whichever reads better. */
144
- get velocityY(): number { return this._state()[1] }
145
- set velocityY(v: number) { if (this._id) _creator2d.characterSetVerticalVelocity(this._id, v) }
146
-
147
- /**
148
- * READ — the velocity the solver ended up with after the most recent step (world units/s, fresh
149
- * Vec2): what HAPPENED, not what you asked for. Walking into a wall reads ~0, sliding along one
150
- * reads the tangent.
151
- *
152
- * WRITE — LATCH the whole velocity: a knockback, a wall jump, a launch pad. Unlike `move()` it does
153
- * not expire, so the character keeps flying, and gravity still pulls the vertical down into a real
154
- * ballistic arc. It stays until `move()` takes the axis back — so a game simply doesn't call
155
- * `move()` while the throw lasts, and ends it on its own terms:
156
- *
157
- * hero.controller.velocity = [dir.x * 600, 400] // hit by the blast
158
- * if (thrown) { if (hero.controller.grounded) thrown = false } // landing ends it
159
- * else hero.controller.move(ix * SPEED) // …and this reclaims the axis
160
- *
161
- * Nothing clears the latch by itself, landing included — a kinematic controller has no friction.
162
- * Reading is not the inverse of writing: against a wall the read is ~0 and would cancel the throw.
163
- *
164
- * `c.velocity.y = 700` (the direct spelling) is a jump — the compiler routes it to the exact
165
- * `velocityY` channel via `_writeComp` below — and `c.velocity.x = 300` latches the whole vector
166
- * with the measured vertical filling in. A STORED copy is still a copy (`const v = c.velocity`).
167
- */
168
- get velocity(): Vec2 { const s = this._state(); return new Vec2(s[0], s[1]) }
169
- set velocity(v: Vec2Like) { if (this._id) _creator2d.characterSetVelocity(this._id, cx(v), cy(v)) }
170
-
171
- /** Compile-time list (chisel reads it, then strips it) — see CharacterController / core/compWrite.ts. */
172
- static _comps = [ "velocity" ]
173
-
174
- /** @internal `c.velocity.<axis> = v` compiles to this; see CharacterController. */
175
- _writeComp(_prop: string, axis: CompAxis, v: number): void {
176
- if (!this._id) return
177
- if (axis === "y") { _creator2d.characterSetVerticalVelocity(this._id, v); return }
178
- const s = this._state()
179
- _creator2d.characterSetVelocity(this._id, axis === "x" ? v : s[0], s[1])
180
- }
181
-
182
- /** True while standing on walkable ground. */
183
- get grounded(): boolean { return this._state()[2] === 0 }
184
-
185
- /** Where the feet are after the last step: walkable ground, too-steep ground, or the air. */
186
- get groundState(): GroundState2D { return GROUND_STATES[this._state()[2]] ?? "air" }
187
-
188
- /** The surface normal under the feet — for orienting a sprite to a slope, or deciding a slide. */
189
- get groundNormal(): Vec2 { const s = this._state(); return new Vec2(s[3], s[4]) }
190
-
191
- /** What the character is standing on: a moving platform, a hazard, an ice patch whose `friction`
192
- * the game can read. The engine already carries the character along a kinematic platform. */
193
- get groundNode(): Node2D | null {
194
- const id = this._state()[5]
195
- return id ? node2dRegistry.get(id) ?? null : null
196
- }
197
-
198
- /**
199
- * Every surface the mover pushed out of during the last step, with the normal pointing back at the
200
- * character. This is what wall jumps and box pushing are written against — a kinematic mover is not
201
- * a body, so those contacts do not arrive as 'enter' events.
202
- *
203
- * const wall = c.collisions.find(h => Math.abs(h.normal.x) > 0.7)
204
- * if (wall && jumpPressed) c.velocity = [-wall.normal.x * KICK, JUMP]
205
- *
206
- * for (const h of c.collisions) h.node?.physics?.applyImpulse([-h.normal.x * PUSH, 0])
207
- */
208
- get collisions(): Collision2D[] {
209
- if (!this._id) return []
210
- const o = _creator2d.characterGetCollisions(this._id, MAX_COLLISIONS)
211
- const out: Collision2D[] = []
212
- for (let i = 0; i + 2 < o.length; i += 3) {
213
- out.push({ node: o[i] ? node2dRegistry.get(o[i]) ?? null : null, normal: new Vec2(o[i + 1], o[i + 2]) })
214
- }
215
- return out
216
- }
217
-
218
- /** Multiplier over the world gravity; default 1. **0 = free mode**: no gravity, no ground, no
219
- * stick-to-floor — a swimmer, a drone, a ladder climber, or a top-down hero, driven by the
220
- * two-component `move()`. */
221
- get gravityScale(): number { return this._gravityScale }
222
- set gravityScale(v: number) {
223
- this._gravityScale = v
224
- if (this._id) _creator2d.characterSetGravityScale(this._id, v)
225
- }
226
-
227
- /** Max ground slope (degrees) the character treats as walkable; default 45. Live. */
228
- get maxSlope(): number { return this._maxSlope }
229
- set maxSlope(v: number) {
230
- this._maxSlope = v
231
- if (this._id) _creator2d.characterSetMaxSlope(this._id, v)
232
- }
233
-
234
- /** Which collision group the character belongs to. Live. */
235
- get group(): PhysicsGroup2D | undefined { return this._group }
236
- set group(g: PhysicsGroup2D | undefined) { this._group = g; this._repush() }
237
-
238
- /**
239
- * While this names a node, that node's `OneWay2D` surfaces are not solid for this character — how
240
- * you drop off a semisolid platform. A plain latch: whoever sets it clears it.
241
- *
242
- * if (downPressed && jumpPressed && c.grounded) {
243
- * const platform = c.groundNode
244
- * c.dropThrough = platform
245
- * setTimeout(() => { if (c.dropThrough === platform) c.dropThrough = null }, 200)
246
- * }
247
- *
248
- * Because it names ONE node, the timeout is not delicate: too long merely means you could have
249
- * re-landed on that platform for a moment, and the platform below stays solid either way.
250
- */
251
- get dropThrough(): Node2D | null { return this._dropThrough }
252
- set dropThrough(n: Node2D | null) {
253
- this._dropThrough = n
254
- if (this._id) _creator2d.characterSetDropThrough(this._id, n ? n.id : 0)
255
- }
256
-
257
- /**
258
- * True while a requested collider resize hasn't taken — you asked to stand up and there is something
259
- * overhead. Resizing goes through the `Shape2D` aspect itself:
260
- *
261
- * hero.aspect(Shape2D, { capsule: CROUCHED }) // always fits — you are shrinking
262
- * hero.aspect(Shape2D, { capsule: STANDING }) // may be refused under a low ceiling
263
- * if (hero.controller.resizing) … // still crouched; call it again next frame
264
- *
265
- * A refusal changes nothing, so the retry is just the same call again — and it is an exact headroom
266
- * test against the real capsule, unlike a hand-rolled raycast (a ray is a line; a capsule has
267
- * girth). The FEET stay planted across a resize, so the character neither hovers nor sinks.
268
- */
269
- get resizing(): boolean { return this.node.get(Shape2D)?._resizeRefused ?? false }
270
-
271
- private _state(): Float32Array {
272
- if (!this._id) { state.fill(0); state[2] = 2; state[4] = 1; return state }
273
- _creator2d.characterGetState(this._id, state)
274
- return state
275
- }
276
- }