lecodes-cli 0.17.2 → 0.18.1
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.
- package/README.md +1 -1
- package/dist/index.js +2376 -755
- package/package.json +4 -4
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +52 -8
- package/runtime/sdk/compile/assetMacro.ts +116 -15
- package/runtime/sdk/compile/bundler.ts +39 -4
- package/runtime/sdk/compile/compileProject.ts +16 -1
- package/runtime/sdk/compile/header.ts +6 -1
- package/runtime/sdk/compile/index.ts +31 -0
- package/runtime/sdk/compile/liteMaterial.ts +247 -0
- package/runtime/sdk/compile/sceneEditor.ts +11 -1
- package/runtime/sdk/compile/shaderSchema.ts +202 -0
- package/runtime/sdk/compile/shaderTargets.ts +81 -0
- package/runtime/sdk/core/Aspect.ts +363 -95
- package/runtime/sdk/core/compWrite.ts +42 -0
- package/runtime/sdk/core/fields.ts +1 -1
- package/runtime/sdk/core/time.ts +81 -0
- package/runtime/sdk/g2/Camera2D.ts +8 -1
- package/runtime/sdk/g2/CharacterController2D.ts +253 -53
- package/runtime/sdk/g2/Node2D.ts +80 -10
- package/runtime/sdk/g2/OneWay2D.ts +66 -0
- package/runtime/sdk/g2/Physics2D.ts +240 -30
- package/runtime/sdk/g2/Scene2D.ts +33 -1
- package/runtime/sdk/g2/Shape2D.ts +218 -22
- package/runtime/sdk/g2/Trigger2D.ts +42 -12
- package/runtime/sdk/g2/groups2d.ts +106 -0
- package/runtime/sdk/g2/loop.ts +15 -4
- package/runtime/sdk/gl/Camera.ts +41 -0
- package/runtime/sdk/gl/CameraPlace.ts +52 -0
- package/runtime/sdk/gl/CharacterController.ts +184 -55
- package/runtime/sdk/gl/Gearbox.ts +212 -0
- package/runtime/sdk/gl/Geometry.ts +70 -9
- package/runtime/sdk/gl/IK.ts +193 -174
- package/runtime/sdk/gl/Light.ts +64 -2
- package/runtime/sdk/gl/Lightmap.ts +179 -0
- package/runtime/sdk/gl/Material.ts +36 -0
- package/runtime/sdk/gl/Mesh.ts +6 -23
- package/runtime/sdk/gl/Model.ts +23 -8
- package/runtime/sdk/gl/Node.ts +350 -285
- package/runtime/sdk/gl/Physics.ts +222 -126
- package/runtime/sdk/gl/Scene.ts +175 -8
- package/runtime/sdk/gl/Shape.ts +255 -12
- package/runtime/sdk/gl/Trigger.ts +1 -6
- package/runtime/sdk/gl/Vehicle.ts +473 -0
- package/runtime/sdk/gl/Wheel.ts +240 -0
- package/runtime/sdk/gl/{AnimationClip.ts → animation/AnimationClip.ts} +37 -7
- package/runtime/sdk/gl/animation/Animator.ts +87 -0
- package/runtime/sdk/gl/animation/Layer.ts +29 -0
- package/runtime/sdk/gl/animation/Loop.ts +25 -0
- package/runtime/sdk/gl/animation/Playback.ts +43 -0
- package/runtime/sdk/gl/animation/core.ts +294 -0
- package/runtime/sdk/gl/scenarios.ts +291 -349
- package/runtime/sdk/inject.ts +186 -162
- package/runtime/sdk/runtime/app.ts +13 -0
- package/runtime/sdk/runtime/input.ts +169 -6
- package/runtime/sdk/scene/defineScene.ts +1227 -1016
- package/runtime/sdk/scene/gizmos.ts +148 -0
- package/runtime/sdk/scene/material.ts +188 -0
- package/runtime/sdk-types.json +1 -1
- package/runtime/sdk/gl/Animator.ts +0 -642
- package/runtime/sdk/gl/ModelAnimation.ts +0 -95
|
@@ -11,23 +11,29 @@
|
|
|
11
11
|
// class Health extends Aspect<'health', Sprite> { hp = 100; hurt(n: number) { this.hp -= n } }
|
|
12
12
|
// sprite.aspect(Health, { hp: 80 }).health.hurt(10)
|
|
13
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
|
+
//
|
|
14
18
|
// The accessor name lives in the generic (Aspect<'name', Node>). The `chisel` bundler extracts it at
|
|
15
19
|
// compile time and injects the runtime `static aspect` on user aspects + the virtual type accessor;
|
|
16
20
|
// the SDK's own built-in aspects declare `static readonly aspect` directly (see SpriteAnimation etc.).
|
|
17
21
|
|
|
22
|
+
import type { Scene } from "../gl/Scene"
|
|
18
23
|
import { Emitter, type EventMap } from "./events"
|
|
19
24
|
import type { InspectorUI } from "./InspectorUI"
|
|
20
25
|
import type { Node } from "../gl/Node"
|
|
26
|
+
import { Time, _registerTimeScaleSink } from "./time"
|
|
21
27
|
|
|
22
28
|
// ---- type helpers -----------------------------------------------------------
|
|
23
29
|
/** The class object of an aspect — accepted even when its constructor is protected/abstract. */
|
|
24
|
-
export type AspectCtor<A extends Aspect<any, any>> = abstract new () => A
|
|
30
|
+
export type AspectCtor<A extends Aspect<any, any, any>> = abstract new () => A
|
|
25
31
|
/** The accessor name carried in the aspect's generic. */
|
|
26
|
-
export type KeyOf<A> = A extends Aspect<infer K, any> ? K : never
|
|
27
|
-
/** The node kind an aspect targets. */
|
|
28
|
-
export type TargetOf<A> = A extends Aspect<any, infer P> ? P : never
|
|
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
|
|
29
35
|
/** `{ name: Aspect }` — the field an aspect contributes to its node. */
|
|
30
|
-
export type FieldOf<A extends Aspect<any, any>> = { [P in KeyOf<A>]: A }
|
|
36
|
+
export type FieldOf<A extends Aspect<any, any, any>> = { [P in KeyOf<A>]: A }
|
|
31
37
|
|
|
32
38
|
type UnionToIntersection<U> =
|
|
33
39
|
(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never
|
|
@@ -37,73 +43,247 @@ type UnionToIntersection<U> =
|
|
|
37
43
|
* English and mirrors the runtime guard `node.has(Health)`:
|
|
38
44
|
* let boss: With<Sprite, Health | Physics>
|
|
39
45
|
*/
|
|
40
|
-
export type With<N, A extends Aspect<any, any>> =
|
|
46
|
+
export type With<N, A extends Aspect<any, any, any>> =
|
|
41
47
|
N & UnionToIntersection<A extends any ? FieldOf<A> : never>
|
|
42
48
|
|
|
43
49
|
// ---- per-frame update dispatch (two phases) ---------------------------------
|
|
44
|
-
// Aspects
|
|
45
|
-
// `
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// •
|
|
49
|
-
// transforms
|
|
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
|
+
//
|
|
50
59
|
// The FRAME SOURCE is pluggable so core never names a specific engine bridge (a 3D-only bundle must not
|
|
51
|
-
// pull in the 2D one):
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
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
|
|
58
87
|
type PhaseFn = (dt: number) => void
|
|
59
|
-
|
|
60
|
-
const lateUpdaters: Updatable[] = []
|
|
61
|
-
let dispatchInstalled = false
|
|
88
|
+
type Ctor = Function & { after?: Function[], before?: Function[], aspect?: string }
|
|
62
89
|
|
|
63
90
|
// A render-synced frame source: given the two phase runners, wire them to the engine and return true;
|
|
64
|
-
// return false if unavailable (→ setLoop fallback). Installed by the
|
|
65
|
-
// never references a concrete engine bridge — that keeps the 2D bridge out of a 3D-only bundle.
|
|
91
|
+
// return false if unavailable (→ setLoop fallback). Installed by the engine layers (Scene / Scene2D).
|
|
66
92
|
type FrameInstaller = (early: PhaseFn, late: PhaseFn) => boolean
|
|
67
93
|
let frameInstaller: FrameInstaller | undefined
|
|
68
|
-
/** @internal Install a render-synced frame source for aspect update(dt). Called from
|
|
94
|
+
/** @internal Install a render-synced frame source for aspect update(dt). Called from an engine layer. */
|
|
69
95
|
export const _installAspectFrames = (fn: FrameInstaller): void => { frameInstaller = fn }
|
|
70
96
|
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
73
109
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|
|
79
154
|
}
|
|
80
155
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
84
193
|
}
|
|
85
194
|
|
|
195
|
+
const earlyUpdaters = new PhaseList()
|
|
196
|
+
const lateUpdaters = new PhaseList()
|
|
197
|
+
let dispatchInstalled = false
|
|
198
|
+
|
|
86
199
|
const ensureDispatch = (): void => {
|
|
87
200
|
if (dispatchInstalled) return
|
|
88
201
|
dispatchInstalled = true
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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).
|
|
92
207
|
if (!(frameInstaller && frameInstaller(early, late))) {
|
|
93
208
|
setLoop((dt) => { early(dt); late(dt) })
|
|
94
209
|
}
|
|
95
210
|
}
|
|
96
211
|
|
|
97
|
-
const registerUpdater = (
|
|
98
|
-
const
|
|
99
|
-
|
|
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")
|
|
100
219
|
ensureDispatch()
|
|
101
220
|
}
|
|
102
|
-
const unregisterUpdater = (
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
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])
|
|
107
287
|
}
|
|
108
288
|
|
|
109
289
|
// ---- the host: mixed into every node kind (Node2D, Node) --------------------
|
|
@@ -114,66 +294,68 @@ const unregisterUpdater = (a: Updatable): void => {
|
|
|
114
294
|
*/
|
|
115
295
|
export abstract class AspectHost<E extends EventMap = EventMap> extends Emitter<E> {
|
|
116
296
|
/** @internal class → instance, the authoritative store (named accessors mirror this). */
|
|
117
|
-
readonly _aspects = new Map<Function, Aspect<any, any>>()
|
|
297
|
+
readonly _aspects = new Map<Function, Aspect<any, any, any>>()
|
|
118
298
|
|
|
119
299
|
/** Attach (and configure) an aspect, or reconfigure it if already present. Returns the node typed
|
|
120
300
|
* as now-having that aspect (so no guard/`?.` is needed afterwards). Rejects a wrong target node. */
|
|
121
|
-
aspect<Self extends TargetOf<A>, A extends Aspect<any, any>>(
|
|
301
|
+
aspect<Self extends TargetOf<A>, A extends Aspect<any, any, any>>(
|
|
122
302
|
this: Self,
|
|
123
303
|
ctor: AspectCtor<A>,
|
|
124
304
|
opts?: Partial<A>,
|
|
125
305
|
): Self & FieldOf<A> {
|
|
126
|
-
|
|
127
|
-
let inst = host._aspects.get(ctor) as A | undefined
|
|
128
|
-
const fresh = inst === undefined
|
|
129
|
-
if (!inst) {
|
|
130
|
-
inst = new (ctor as unknown as new () => A)()
|
|
131
|
-
;(inst as { node: unknown }).node = this
|
|
132
|
-
host._aspects.set(ctor, inst)
|
|
133
|
-
const name = (ctor as { aspect?: string }).aspect
|
|
134
|
-
if (name) (host as unknown as Record<string, unknown>)[name] = inst
|
|
135
|
-
}
|
|
136
|
-
if (opts) Object.assign(inst, opts)
|
|
137
|
-
if (fresh) {
|
|
138
|
-
inst.onAttach?.()
|
|
139
|
-
if (typeof inst.update === "function") registerUpdater(inst)
|
|
140
|
-
}
|
|
141
|
-
return this as Self & FieldOf<A>
|
|
306
|
+
return attachTo(this, (this as unknown as AspectHost)._aspects, ctor, opts, undefined)
|
|
142
307
|
}
|
|
143
308
|
|
|
144
309
|
/** Safe access — undefined if the aspect isn't attached. */
|
|
145
|
-
get<A extends Aspect<any, any>>(ctor: AspectCtor<A>): A | undefined {
|
|
310
|
+
get<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): A | undefined {
|
|
146
311
|
return this._aspects.get(ctor) as A | undefined
|
|
147
312
|
}
|
|
148
313
|
|
|
149
314
|
/** Existence check AND type guard: inside `if (node.has(Physics))`, `node.physics` is present. */
|
|
150
|
-
has<A extends Aspect<any, any>>(ctor: AspectCtor<A>): this is this & FieldOf<A> {
|
|
315
|
+
has<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): this is this & FieldOf<A> {
|
|
151
316
|
return this._aspects.has(ctor)
|
|
152
317
|
}
|
|
153
318
|
|
|
154
319
|
/** Detach an aspect (runs its onDetach). Named `removeAspect` so it never collides with the
|
|
155
320
|
* child-management `add`/`remove` semantics some node kinds expose. */
|
|
156
|
-
removeAspect<A extends Aspect<any, any>>(ctor: AspectCtor<A>): this {
|
|
157
|
-
|
|
158
|
-
if (inst) {
|
|
159
|
-
inst.onDetach?.()
|
|
160
|
-
unregisterUpdater(inst)
|
|
161
|
-
this._aspects.delete(ctor)
|
|
162
|
-
const name = (ctor as { aspect?: string }).aspect
|
|
163
|
-
if (name) delete (this as unknown as Record<string, unknown>)[name]
|
|
164
|
-
}
|
|
321
|
+
removeAspect<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): this {
|
|
322
|
+
detachFrom(this, this._aspects, ctor)
|
|
165
323
|
return this
|
|
166
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
|
+
}
|
|
167
331
|
}
|
|
168
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
|
+
|
|
169
349
|
// ---- the aspect base --------------------------------------------------------
|
|
170
350
|
/**
|
|
171
351
|
* Base for everything you attach. `K` = accessor name (extracted by chisel for the runtime). `P` =
|
|
172
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.
|
|
173
355
|
* Initialize in `onAttach` (the node is set by then) — not a constructor; aspects are created by the
|
|
174
356
|
* engine via `node.aspect()`, never `new`.
|
|
175
357
|
*/
|
|
176
|
-
export abstract class Aspect<K extends string, P = Node> {
|
|
358
|
+
export abstract class Aspect<K extends string, P = Node, E extends EventMap = {}> {
|
|
177
359
|
/**
|
|
178
360
|
* Opt this aspect class into EDITOR-RUN mode (generators): while a scene is edited, the scene
|
|
179
361
|
* loader constructs the instance (refs resolved, `node`/`generated` set — never `onAttach`) and
|
|
@@ -195,12 +377,25 @@ export abstract class Aspect<K extends string, P = Node> {
|
|
|
195
377
|
*/
|
|
196
378
|
static inspector?: (ui: InspectorUI, aspect: any) => void
|
|
197
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
|
+
|
|
198
393
|
// NOTE: P has no `extends AspectHost` constraint on purpose — Emitter<E>'s generic
|
|
199
394
|
// addEventListener makes a specific-event node (AspectHost<NodeEvents>) not assignable to
|
|
200
395
|
// AspectHost<any>, which would reject every real node. P only types `this.node`; target
|
|
201
396
|
// enforcement happens in aspect() via `this: Self extends TargetOf<A>`. Defaults to Node
|
|
202
397
|
// (3D is the default engine) — a 2D aspect declares its own P, e.g. Aspect<'x', Sprite>.
|
|
203
|
-
/** The node this aspect is attached to (set at attach time). */
|
|
398
|
+
/** The node this aspect is attached to (set at attach time). For a System: the scene. */
|
|
204
399
|
readonly node!: P
|
|
205
400
|
/**
|
|
206
401
|
* GENERATOR aspects (`static editor = { rebuild: true }`, attached through a scene file): a
|
|
@@ -209,11 +404,36 @@ export abstract class Aspect<K extends string, P = Node> {
|
|
|
209
404
|
* the scene's draw set automatically (membership is separate from parenting). Undefined for
|
|
210
405
|
* aspects attached by hand outside scene files. See docs/3d/scene-files.md.
|
|
211
406
|
*/
|
|
212
|
-
readonly generated!: Node & { clear(): void }
|
|
213
|
-
/**
|
|
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.
|
|
214
420
|
declare protected readonly __key__?: K
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
217
437
|
/**
|
|
218
438
|
* GENERATOR aspects: (re)build the derived output under `this.generated` — must be idempotent
|
|
219
439
|
* (clear, then create). Call it from `onAttach()` for play mode; a class opting in with
|
|
@@ -222,23 +442,71 @@ export abstract class Aspect<K extends string, P = Node> {
|
|
|
222
442
|
*/
|
|
223
443
|
rebuild?(): void
|
|
224
444
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* step to consume them. Cameras/followers READ, so they stay in the default LATE phase.
|
|
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`).
|
|
231
450
|
*/
|
|
232
|
-
update?(dt: number): void
|
|
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
|
|
233
459
|
|
|
234
|
-
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
|
|
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
|
|
240
467
|
|
|
241
468
|
/** Opt-in: only run update(dt) while the node is on-screen. NOOP for now — visibility culling isn't
|
|
242
469
|
* wired yet, so every updater ticks regardless; declared so aspects can opt in ahead of it. */
|
|
243
|
-
updateWhenVisible = false
|
|
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.
|
|
244
512
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Component writes on SDK-owned vectors — the runtime half of chisel's `comp_write` pass.
|
|
2
|
+
//
|
|
3
|
+
// hero.controller.velocity.y = 7 → __compWrite(hero.controller, "velocity", "y", 7)
|
|
4
|
+
// hero.controller.velocity.y += 3 → __compOp(hero.controller, "velocity", "y", 0, 3)
|
|
5
|
+
//
|
|
6
|
+
// A getter like `velocity` hands out a fresh Vec3, so the literal spelling would write to a copy and
|
|
7
|
+
// silently do nothing. chisel rewrites the exact `<owner>.<prop>.<x|y|z|w> = v` shape into these
|
|
8
|
+
// helpers, which ask the OWNER to apply the component write (no getter call, no allocation) and
|
|
9
|
+
// fall back to the plain write on anything else, so a user record `{ velocity: { x, y } }` behaves
|
|
10
|
+
// exactly as written. Only the direct spelling is covered: `const v = c.velocity; v.y = 7` is still
|
|
11
|
+
// a write to a copy.
|
|
12
|
+
//
|
|
13
|
+
// Owners today: Node / Node2D / Camera2D (`position` → the scalar setters, physics routing included),
|
|
14
|
+
// Physics (`velocity`, `angularVelocity`), CharacterController / CharacterController2D (`velocity`).
|
|
15
|
+
//
|
|
16
|
+
// An owner declares WHICH getters qualify as compile-time data and answers them at runtime:
|
|
17
|
+
//
|
|
18
|
+
// static _comps = ["velocity"] // chisel reads the list, then strips it (0 bytes shipped)
|
|
19
|
+
// _writeComp(prop, axis, value) { … } // called for listed props only; a hook without the
|
|
20
|
+
// // list is a build diagnostic and routes nothing
|
|
21
|
+
|
|
22
|
+
export type CompAxis = "x" | "y" | "z" | "w"
|
|
23
|
+
|
|
24
|
+
/** Implemented by a class whose vector getters accept component writes (see above). The class also
|
|
25
|
+
* carries `static _comps: string[]` naming those getters — compile-time data, absent at runtime. */
|
|
26
|
+
export interface CompWriter {
|
|
27
|
+
_writeComp(prop: string, axis: CompAxis, value: number): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `<o>.<prop>.<axis> = value`, routed to the owner when it has the hook. Returns the value. */
|
|
31
|
+
export const __compWrite = (o: any, prop: string, axis: CompAxis, value: number): number => {
|
|
32
|
+
if (o._writeComp !== undefined) o._writeComp(prop, axis, value) // a direct call, not .call — cheaper on QuickJS
|
|
33
|
+
else o[prop][axis] = value
|
|
34
|
+
return value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `<o>.<prop>.<axis> op= v` — `op`: 0 `+=`, 1 `-=`, 2 `*=`, 3 `/=`. The current component is read
|
|
38
|
+
* through the normal getter (one fresh vector), the result written like `__compWrite`. */
|
|
39
|
+
export const __compOp = (o: any, prop: string, axis: CompAxis, op: number, v: number): number => {
|
|
40
|
+
const cur = o[prop][axis] as number
|
|
41
|
+
return __compWrite(o, prop, axis, op === 0 ? cur + v : op === 1 ? cur - v : op === 2 ? cur * v : cur / v)
|
|
42
|
+
}
|
|
@@ -62,7 +62,7 @@ export type AspectClassInfo = {
|
|
|
62
62
|
|
|
63
63
|
// Instance fields every aspect inherits from the Aspect base — configuration plumbing, not content.
|
|
64
64
|
// `generated` is declare-only in TS, but the bundler lowers it to a real (undefined) class field.
|
|
65
|
-
const BASE_FIELDS = new Set([ "node", "generated", "
|
|
65
|
+
const BASE_FIELDS = new Set([ "node", "generated", "updateOrder", "updateWhilePaused", "updateWhenVisible" ])
|
|
66
66
|
|
|
67
67
|
/** Infer the editor widget from a default value's shape. */
|
|
68
68
|
export const inferFieldEditor = (value: unknown): FieldEditor | undefined => {
|