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.
- package/dist/index.js +1866 -301
- package/package.json +14 -12
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +891 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +326 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +448 -0
- package/src/commands/appTemplates.ts +883 -0
- package/src/commands/appTemplatesAndroid.ts +599 -0
- package/src/commands/appTemplatesGradlew.ts +9 -0
- package/src/commands/assets.ts +28 -0
- package/src/commands/clone.ts +60 -0
- package/src/commands/compile.ts +253 -0
- package/src/commands/create.ts +59 -0
- package/src/commands/design.ts +863 -0
- package/src/commands/designTemplates.ts +11 -0
- package/src/commands/desktop.ts +122 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/diff.ts +59 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/install.ts +198 -0
- package/src/commands/lightmap.ts +290 -0
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/navmesh.ts +225 -0
- package/src/commands/pn.ts +305 -0
- package/src/commands/projectTemplates.ts +129 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +414 -0
- package/src/commands/scene.ts +148 -0
- package/src/commands/shaders.ts +157 -0
- package/src/commands/status.ts +29 -0
- package/src/commands/test.ts +370 -0
- package/src/commands/thumbs.ts +178 -0
- package/src/commands/types.ts +87 -0
- package/src/commands/update.ts +190 -0
- package/src/compile/assetIcons.ts +214 -0
- package/src/compile/collect.ts +46 -0
- package/src/compile/collectLocal.ts +37 -0
- package/src/compile/designCompile.ts +104 -0
- package/src/compile/fonts.ts +156 -0
- package/src/compile/headlessBundle.ts +129 -0
- package/src/compile/nativeStack.ts +21 -0
- package/src/compile/projectCompile.ts +109 -0
- package/src/compile/sceneCompile.ts +113 -0
- package/src/compile/screenEntry.ts +127 -0
- package/src/compile/shaders.ts +245 -0
- package/src/config.ts +42 -0
- package/src/designMeta.ts +35 -0
- package/src/desktopRenderer.ts +532 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +292 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +491 -0
- package/src/lecodes-3d-editor.d.ts +41 -0
- package/src/lecodes-assets.d.ts +7 -0
- package/src/lecodes-design.d.ts +191 -0
- package/src/lecodes-renderer.d.ts +131 -0
- package/src/localFiles.ts +144 -0
- package/src/manifest.ts +46 -0
- package/src/matcTool.ts +137 -0
- package/src/peers.ts +40 -0
- package/src/project.ts +61 -0
- package/src/projectEnv.ts +94 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/releases.ts +125 -0
- package/src/serverDiff.ts +78 -0
- package/src/textDiff.ts +103 -0
- package/src/types.ts +0 -0
- package/src/util.ts +146 -0
- package/runtime/sdk/animate/animate.ts +0 -238
- package/runtime/sdk/animate/bezier.ts +0 -138
- package/runtime/sdk/animate/easings.ts +0 -126
- package/runtime/sdk/canvas/Canvas.ts +0 -305
- package/runtime/sdk/core/Aspect.ts +0 -512
- package/runtime/sdk/core/InspectorUI.ts +0 -212
- package/runtime/sdk/core/color.ts +0 -66
- package/runtime/sdk/core/compWrite.ts +0 -42
- package/runtime/sdk/core/events.ts +0 -38
- package/runtime/sdk/core/fields.ts +0 -120
- package/runtime/sdk/core/registry.ts +0 -23
- package/runtime/sdk/core/signals.ts +0 -277
- package/runtime/sdk/core/time.ts +0 -81
- package/runtime/sdk/g2/Camera2D.ts +0 -40
- package/runtime/sdk/g2/CharacterController2D.ts +0 -276
- package/runtime/sdk/g2/Node2D.ts +0 -267
- package/runtime/sdk/g2/OneWay2D.ts +0 -66
- package/runtime/sdk/g2/Physics2D.ts +0 -346
- package/runtime/sdk/g2/Scene2D.ts +0 -209
- package/runtime/sdk/g2/Shape2D.ts +0 -259
- package/runtime/sdk/g2/Sprite.ts +0 -89
- package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
- package/runtime/sdk/g2/SpriteSheet.ts +0 -166
- package/runtime/sdk/g2/Texture2D.ts +0 -47
- package/runtime/sdk/g2/Tilemap.ts +0 -41
- package/runtime/sdk/g2/Tileset.ts +0 -71
- package/runtime/sdk/g2/Trigger2D.ts +0 -77
- package/runtime/sdk/g2/autotile.ts +0 -433
- package/runtime/sdk/g2/cells.ts +0 -91
- package/runtime/sdk/g2/defineScene2d.ts +0 -381
- package/runtime/sdk/g2/groups2d.ts +0 -106
- package/runtime/sdk/g2/loop.ts +0 -50
- package/runtime/sdk/g2/scenarios2d.ts +0 -69
- package/runtime/sdk/g2/touch.ts +0 -83
- package/runtime/sdk/gl/Camera.ts +0 -160
- package/runtime/sdk/gl/CameraPlace.ts +0 -52
- package/runtime/sdk/gl/CharacterController.ts +0 -238
- package/runtime/sdk/gl/Gearbox.ts +0 -212
- package/runtime/sdk/gl/Geometry.ts +0 -279
- package/runtime/sdk/gl/IK.ts +0 -193
- package/runtime/sdk/gl/InstancedMesh.ts +0 -132
- package/runtime/sdk/gl/Light.ts +0 -99
- package/runtime/sdk/gl/Lightmap.ts +0 -179
- package/runtime/sdk/gl/Material.ts +0 -245
- package/runtime/sdk/gl/Mesh.ts +0 -83
- package/runtime/sdk/gl/Model.ts +0 -64
- package/runtime/sdk/gl/Node.ts +0 -350
- package/runtime/sdk/gl/Noise.ts +0 -30
- package/runtime/sdk/gl/Particles.ts +0 -676
- package/runtime/sdk/gl/Physics.ts +0 -222
- package/runtime/sdk/gl/Plane.ts +0 -53
- package/runtime/sdk/gl/Ray.ts +0 -16
- package/runtime/sdk/gl/Scene.ts +0 -479
- package/runtime/sdk/gl/Shape.ts +0 -377
- package/runtime/sdk/gl/Texture.ts +0 -46
- package/runtime/sdk/gl/Trigger.ts +0 -45
- package/runtime/sdk/gl/Vehicle.ts +0 -473
- package/runtime/sdk/gl/Wheel.ts +0 -240
- package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
- package/runtime/sdk/gl/animation/Animator.ts +0 -87
- package/runtime/sdk/gl/animation/Layer.ts +0 -29
- package/runtime/sdk/gl/animation/Loop.ts +0 -25
- package/runtime/sdk/gl/animation/Playback.ts +0 -43
- package/runtime/sdk/gl/animation/core.ts +0 -294
- package/runtime/sdk/gl/controls.ts +0 -95
- package/runtime/sdk/gl/physicsEvents.ts +0 -20
- package/runtime/sdk/gl/scenarios.ts +0 -291
- package/runtime/sdk/gl/state.ts +0 -6
- package/runtime/sdk/gl/touch.ts +0 -68
- package/runtime/sdk/inject.ts +0 -186
- package/runtime/sdk/math/Mathf.ts +0 -118
- package/runtime/sdk/math/mat4.ts +0 -278
- package/runtime/sdk/math/quat.ts +0 -232
- package/runtime/sdk/math/vec.ts +0 -255
- package/runtime/sdk/plugins/camera.ts +0 -81
- package/runtime/sdk/plugins/geolocation.ts +0 -123
- package/runtime/sdk/plugins/oauth.ts +0 -61
- package/runtime/sdk/plugins/permission.ts +0 -7
- package/runtime/sdk/plugins/push.ts +0 -132
- package/runtime/sdk/plugins/qr.ts +0 -73
- package/runtime/sdk/plugins/service.ts +0 -47
- package/runtime/sdk/runtime/app.ts +0 -101
- package/runtime/sdk/runtime/appEvents.ts +0 -54
- package/runtime/sdk/runtime/channel.ts +0 -61
- package/runtime/sdk/runtime/clipboard.ts +0 -20
- package/runtime/sdk/runtime/datetime.ts +0 -329
- package/runtime/sdk/runtime/device.ts +0 -293
- package/runtime/sdk/runtime/fetch.ts +0 -77
- package/runtime/sdk/runtime/files.ts +0 -23
- package/runtime/sdk/runtime/input.ts +0 -175
- package/runtime/sdk/runtime/media.ts +0 -111
- package/runtime/sdk/runtime/misc.ts +0 -16
- package/runtime/sdk/runtime/net.ts +0 -36
- package/runtime/sdk/runtime/rpc.ts +0 -218
- package/runtime/sdk/runtime/service.ts +0 -83
- package/runtime/sdk/runtime/share.ts +0 -9
- package/runtime/sdk/runtime/storage.ts +0 -13
- package/runtime/sdk/runtime/touch.ts +0 -76
- package/runtime/sdk/scene/defineScene.ts +0 -1227
- package/runtime/sdk/scene/editorPlugins.ts +0 -92
- package/runtime/sdk/scene/gizmos.ts +0 -148
- package/runtime/sdk/scene/grammar.ts +0 -120
- package/runtime/sdk/scene/material.ts +0 -188
- package/runtime/sdk/server/auth/appConfig.ts +0 -12
- package/runtime/sdk/server/auth/global.ts +0 -80
- package/runtime/sdk/server/auth/host.ts +0 -318
- package/runtime/sdk/server/auth/models.ts +0 -83
- package/runtime/sdk/server/auth/types.ts +0 -50
- package/runtime/sdk/server/channel.ts +0 -56
- package/runtime/sdk/server/context.ts +0 -36
- package/runtime/sdk/server/db/defineDb.ts +0 -237
- package/runtime/sdk/server/db/fields.ts +0 -132
- package/runtime/sdk/server/db/httpTransport.ts +0 -93
- package/runtime/sdk/server/db/index.ts +0 -7
- package/runtime/sdk/server/db/marci/query.ts +0 -412
- package/runtime/sdk/server/db/types.ts +0 -202
- package/runtime/sdk/server/errors.ts +0 -12
- package/runtime/sdk/server/host.ts +0 -74
- package/runtime/sdk/server/inject.ts +0 -13
- package/runtime/sdk/server/runtime.ts +0 -133
- package/runtime/sdk/server/validate.ts +0 -87
- package/runtime/sdk/ui/NativeView.ts +0 -142
- package/runtime/sdk/ui/UI.ts +0 -39
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
- package/runtime/sdk/ui/UIButton.ts +0 -101
- package/runtime/sdk/ui/UIContainer.ts +0 -60
- package/runtime/sdk/ui/UIImage.ts +0 -83
- package/runtime/sdk/ui/UIInput.ts +0 -185
- package/runtime/sdk/ui/UIModal.ts +0 -139
- package/runtime/sdk/ui/UINode.ts +0 -826
- package/runtime/sdk/ui/UIPager.ts +0 -362
- package/runtime/sdk/ui/UIPopover.ts +0 -100
- package/runtime/sdk/ui/UIScreen.ts +0 -123
- package/runtime/sdk/ui/UIScrollable.ts +0 -87
- package/runtime/sdk/ui/UISpacer.ts +0 -14
- package/runtime/sdk/ui/UITabs.ts +0 -236
- package/runtime/sdk/ui/UIText.ts +0 -51
- package/runtime/sdk/ui/UIVideo.ts +0 -88
- package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
- package/runtime/sdk/ui/UIWidget.ts +0 -127
- package/runtime/sdk/ui/fonts.ts +0 -13
- package/runtime/sdk/ui/presentable.ts +0 -117
- package/runtime/sdk/ui/router.ts +0 -132
- package/runtime/sdk/ui/theme.ts +0 -84
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
// 2D scenes as data: the runtime behind `.scene2d.ts` files (docs/scene2d-editor-plan.md §4).
|
|
2
|
-
// The 2D sibling of scene/defineScene.ts — same literal grammar (one default-exported
|
|
3
|
-
// `defineScene2d({...})` call, nodes keyed by name, `use(...)` aspects, `make(...)` factories),
|
|
4
|
-
// with the 2D source vocabulary: `sprite` (a named entry of an imported `.sprite.ts` sheet),
|
|
5
|
-
// `tilemap` (texture + grid + a `cells('…')` payload), `camera`, `make`, or none = group.
|
|
6
|
-
//
|
|
7
|
-
// // forest.scene2d.ts
|
|
8
|
-
// import props from './props.sprite'
|
|
9
|
-
// import hero from './hero.sprite'
|
|
10
|
-
// export default defineScene2d({
|
|
11
|
-
// env: { background: '#5a6b3b', pixelPerfect: true, gravity: [0, 0] },
|
|
12
|
-
// layers: { 1: { ySort: true } },
|
|
13
|
-
// nodes: {
|
|
14
|
-
// ground: { tilemap: { texture: asset('./TileSet.png'), tile: 64, atlas: [64, 64],
|
|
15
|
-
// cells: cells('…') }, layer: 0 },
|
|
16
|
-
// tree1: { sprite: { sheet: props, name: 'tree_orange' }, position: [160, 288], layer: 1 },
|
|
17
|
-
// player: { sprite: { sheet: hero, clip: 'idle' }, position: [800, 200], layer: 1,
|
|
18
|
-
// aspects: [use(CameraFollow, { smooth: 0.12 })] },
|
|
19
|
-
// },
|
|
20
|
-
// })
|
|
21
|
-
//
|
|
22
|
-
// // main.ts
|
|
23
|
-
// import forest from './forest.scene2d'
|
|
24
|
-
// const { scene, nodes } = await forest.open()
|
|
25
|
-
//
|
|
26
|
-
// Colliders and bodies are SCENE semantics (sheets are art only): give a node `aspects:
|
|
27
|
-
// [use(Shape2D, {…}), use(Physics2D, {…})]`. Aspects attach normally in play mode; in edit mode
|
|
28
|
-
// (`globalThis.__lecodesSceneEdit`) they are held as data, so nothing falls or fires while
|
|
29
|
-
// editing — clip playback still runs (render-side).
|
|
30
|
-
// Prefabs (scene-in-scene) are a later phase — not in this grammar yet.
|
|
31
|
-
|
|
32
|
-
import type { ColorInput } from "../core/color"
|
|
33
|
-
import type { Vec2Like } from "../math/vec"
|
|
34
|
-
import {
|
|
35
|
-
EDIT_FLAG, isEditMode, resolveRefs, type AspectEntry, type MakeEntry as SharedMakeEntry,
|
|
36
|
-
} from "../scene/grammar"
|
|
37
|
-
import { Scene2D } from "./Scene2D"
|
|
38
|
-
import { Node2D } from "./Node2D"
|
|
39
|
-
import { Sprite } from "./Sprite"
|
|
40
|
-
import { Tilemap } from "./Tilemap"
|
|
41
|
-
import { Texture2D } from "./Texture2D"
|
|
42
|
-
import { Physics2D } from "./Physics2D"
|
|
43
|
-
import { SpriteSheet } from "./SpriteSheet"
|
|
44
|
-
import { Tileset, Autotile2D } from "./Tileset"
|
|
45
|
-
import type { SpriteAnimation } from "./SpriteAnimation"
|
|
46
|
-
import type { CellsData } from "./cells"
|
|
47
|
-
import type { With } from "../core/Aspect"
|
|
48
|
-
|
|
49
|
-
// ---- the literal grammar (what the visual editor reads and writes) -----------
|
|
50
|
-
|
|
51
|
-
export type Scene2dEnv = {
|
|
52
|
-
/** Background color — '#5a6b3b', 0x5a6b3b, or [r,g,b] in 0..1. */
|
|
53
|
-
background?: ColorInput
|
|
54
|
-
/** Texture sampling: 'nearest' (crisp pixel art, engine default) or 'linear'. */
|
|
55
|
-
filter?: "nearest" | "linear"
|
|
56
|
-
/** Pixel-art crispness: snap the camera to whole screen pixels (seeds CameraFollow's
|
|
57
|
-
* `pixelSnap`, and the initial camera position lands on whole world units). */
|
|
58
|
-
pixelPerfect?: boolean
|
|
59
|
-
/** Physics world gravity (Y-up: down is negative). Presence configures the world before any
|
|
60
|
-
* body is created — `[0, 0]` for top-down worlds. Omit to leave the world unconfigured. */
|
|
61
|
-
gravity?: Vec2Like
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** A sprite node: one named entry of an imported `.sprite.ts` sheet. The sheet is ART only —
|
|
65
|
-
* colliders/bodies are scene semantics: put Shape2D / Physics2D / Trigger2D in `aspects`. */
|
|
66
|
-
export type SpriteSourceDef = {
|
|
67
|
-
/** The imported sheet handle: `import props from './props.sprite'` … `sheet: props`. */
|
|
68
|
-
sheet: SpriteSheet
|
|
69
|
-
/** The entry name; omit when the sheet declares exactly one sprite. */
|
|
70
|
-
name?: string
|
|
71
|
-
/** Start this clip immediately (sliced entries with clips). */
|
|
72
|
-
clip?: string
|
|
73
|
-
/** Initial facing for `perDirection` clips. */
|
|
74
|
-
direction?: string
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export type TilemapSourceDef = {
|
|
78
|
-
/** The tileset image — `asset('./TileSet.png')`. */
|
|
79
|
-
texture: string
|
|
80
|
-
/** On-screen cell size in world units — a number (square) or [w, h]. */
|
|
81
|
-
tile: number | [number, number]
|
|
82
|
-
/** Atlas grid: how the texture slices into tiles, [atlasCols, atlasRows]. */
|
|
83
|
-
atlas: [number, number]
|
|
84
|
-
/** The map: a `cells('…')` payload — or any `{ cols, rows, data }` of your own. */
|
|
85
|
-
cells: CellsData | { cols: number, rows: number, data: number[] | Int32Array }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** A camera marker node: seeds the scene camera's starting position (+ zoom). To MOVE the camera
|
|
89
|
-
* in play, attach `CameraFollow` to the node being followed — not aspects on this marker. */
|
|
90
|
-
export type Camera2dNodeDef = { zoom?: number }
|
|
91
|
-
|
|
92
|
-
export type Scene2dNodeDef = {
|
|
93
|
-
// -- source (at most one; none = plain group node) --
|
|
94
|
-
sprite?: SpriteSourceDef
|
|
95
|
-
tilemap?: TilemapSourceDef
|
|
96
|
-
camera?: Camera2dNodeDef
|
|
97
|
-
/** A code-built subtree — `make(factoryFn, { ...literal args })`, factory returns a Node2D. */
|
|
98
|
-
make?: SharedMakeEntry<any, Node2D>
|
|
99
|
-
// -- transform / render --
|
|
100
|
-
position?: Vec2Like
|
|
101
|
-
/** Degrees, CCW. */
|
|
102
|
-
rotation?: number
|
|
103
|
-
scale?: Vec2Like | number
|
|
104
|
-
/** Draw layer (int) — higher renders on top. */
|
|
105
|
-
layer?: number
|
|
106
|
-
/** Intra-layer depth for non-Y-sorted layers. */
|
|
107
|
-
z?: number
|
|
108
|
-
/** Sprite tint (sprite nodes only). */
|
|
109
|
-
color?: ColorInput
|
|
110
|
-
/** Sprite opacity 0..1 (sprite nodes only). */
|
|
111
|
-
opacity?: number
|
|
112
|
-
flipX?: boolean
|
|
113
|
-
flipY?: boolean
|
|
114
|
-
visible?: boolean
|
|
115
|
-
/** Editor-only: the move gizmo won't target this node (fields still edit). No runtime effect. */
|
|
116
|
-
locked?: boolean
|
|
117
|
-
// -- capabilities / hierarchy --
|
|
118
|
-
aspects?: readonly AspectEntry<any>[]
|
|
119
|
-
children?: Record<string, Scene2dNodeDef>
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export type Scene2dCameraDef = {
|
|
123
|
-
position?: Vec2Like
|
|
124
|
-
zoom?: number
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export type Scene2dDef = {
|
|
128
|
-
env?: Scene2dEnv
|
|
129
|
-
/** Per-layer config, keyed by layer number: `layers: { 1: { ySort: true } }`. */
|
|
130
|
-
layers?: Record<number | string, { ySort?: boolean }>
|
|
131
|
-
/** Starting camera when there's no camera NODE (a camera node wins). */
|
|
132
|
-
camera?: Scene2dCameraDef
|
|
133
|
-
nodes?: Record<string, Scene2dNodeDef>
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ---- typed handle -------------------------------------------------------------
|
|
137
|
-
|
|
138
|
-
type SourceNodeOf<N extends Scene2dNodeDef> =
|
|
139
|
-
N extends { sprite: { clip: string } } ? Sprite & { anim: SpriteAnimation }
|
|
140
|
-
: N extends { sprite: SpriteSourceDef } ? Sprite
|
|
141
|
-
: N extends { tilemap: TilemapSourceDef } ? Tilemap
|
|
142
|
-
: Node2D
|
|
143
|
-
|
|
144
|
-
type AspectsOf<N extends Scene2dNodeDef> =
|
|
145
|
-
N extends { aspects: readonly AspectEntry<infer A>[] } ? A : never
|
|
146
|
-
|
|
147
|
-
type NodeOf<N extends Scene2dNodeDef> =
|
|
148
|
-
[AspectsOf<N>] extends [never] ? SourceNodeOf<N> : With<SourceNodeOf<N>, AspectsOf<N>>
|
|
149
|
-
|
|
150
|
-
type UnionToIntersection<U> =
|
|
151
|
-
(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never
|
|
152
|
-
|
|
153
|
-
type ChildMapsOf<T extends Record<string, Scene2dNodeDef>> =
|
|
154
|
-
{ [K in keyof T]: T[K] extends { children: infer C extends Record<string, Scene2dNodeDef> } ? NodesOf<C> : never }[keyof T]
|
|
155
|
-
|
|
156
|
-
type NodesOf<T extends Record<string, Scene2dNodeDef>> =
|
|
157
|
-
{ [K in keyof T]: NodeOf<T[K]> } &
|
|
158
|
-
([ChildMapsOf<T>] extends [never] ? unknown : UnionToIntersection<ChildMapsOf<T>>)
|
|
159
|
-
|
|
160
|
-
export type Scene2dNodes<D extends Scene2dDef> =
|
|
161
|
-
D["nodes"] extends Record<string, Scene2dNodeDef> ? NodesOf<D["nodes"]> : Record<string, Node2D>
|
|
162
|
-
|
|
163
|
-
export type LoadedScene2d<D extends Scene2dDef> = {
|
|
164
|
-
scene: Scene2D
|
|
165
|
-
nodes: Scene2dNodes<D>
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ---- runtime -------------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
const createSprite = (name: string, def: SpriteSourceDef): Sprite => {
|
|
171
|
-
const sheet = def.sheet
|
|
172
|
-
if (!(sheet instanceof SpriteSheet)) {
|
|
173
|
-
throw new Error(`Scene2d node "${name}": sprite.sheet is not a sheet handle (import the .sprite file's default export)`)
|
|
174
|
-
}
|
|
175
|
-
const entryName = def.name ?? (sheet.spriteNames.length === 1 ? sheet.spriteNames[0]! : undefined)
|
|
176
|
-
if (entryName === undefined) {
|
|
177
|
-
throw new Error(`Scene2d node "${name}": sprite.name is required — the sheet has ${sheet.spriteNames.length} sprites (${sheet.spriteNames.join(", ")})`)
|
|
178
|
-
}
|
|
179
|
-
return sheet.make(entryName, {
|
|
180
|
-
clip: def.clip,
|
|
181
|
-
direction: def.direction,
|
|
182
|
-
} as Parameters<SpriteSheet["make"]>[1])
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const createTilemap = async (
|
|
186
|
-
name: string, def: TilemapSourceDef, aspects?: readonly AspectEntry<any>[],
|
|
187
|
-
): Promise<Tilemap> => {
|
|
188
|
-
const grid = def.cells
|
|
189
|
-
if (!grid || typeof grid !== "object" || typeof (grid as CellsData).cols !== "number") {
|
|
190
|
-
throw new Error(`Scene2d node "${name}": tilemap.cells must be a cells('…') payload or a { cols, rows, data } value`)
|
|
191
|
-
}
|
|
192
|
-
// Autotile2D on the node flips `cells` from raw atlas indices to material ids — derive the
|
|
193
|
-
// display indices HERE, before the Tilemap exists (aspects are data on the def), so the native
|
|
194
|
-
// layer keeps receiving a dumb index grid and derived data is never persisted.
|
|
195
|
-
let data: number[] | Int32Array = grid.data
|
|
196
|
-
for (const entry of aspects ?? []) {
|
|
197
|
-
if ((entry.ctor as unknown) !== Autotile2D) continue
|
|
198
|
-
const props = entry.props as Partial<Autotile2D> | undefined
|
|
199
|
-
if (props?.tileset instanceof Tileset) {
|
|
200
|
-
data = props.tileset.derive(grid.cols, grid.rows, data, props.seed ?? 0)
|
|
201
|
-
} else {
|
|
202
|
-
console.error(`[scene2d] node "${name}": Autotile2D has no tileset handle — cells render as raw indices`)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
const texture = await Texture2D.load(def.texture)
|
|
206
|
-
return new Tilemap({
|
|
207
|
-
texture, cols: grid.cols, rows: grid.rows,
|
|
208
|
-
tile: def.tile, atlas: def.atlas, data,
|
|
209
|
-
})
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const createSource = (name: string, def: Scene2dNodeDef): Node2D | Promise<Node2D> => {
|
|
213
|
-
const sources = [ def.sprite, def.tilemap, def.camera, def.make ].filter((s) => s !== undefined).length
|
|
214
|
-
if (sources > 1) throw new Error(`Scene2d node "${name}" declares more than one source (sprite/tilemap/camera/make)`)
|
|
215
|
-
if (def.sprite !== undefined) return createSprite(name, def.sprite)
|
|
216
|
-
if (def.tilemap !== undefined) return createTilemap(name, def.tilemap, def.aspects)
|
|
217
|
-
// make/camera/group nodes are plain wrappers — make subtrees mount under them in phase 2
|
|
218
|
-
return new Node2D()
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const applyNode = (node: Node2D, name: string, def: Scene2dNodeDef): void => {
|
|
222
|
-
node.name = name
|
|
223
|
-
if (def.position) node.position = def.position
|
|
224
|
-
if (def.rotation !== undefined) node.rotation = def.rotation
|
|
225
|
-
if (def.scale !== undefined) node.scale = def.scale
|
|
226
|
-
if (def.layer !== undefined) node.layer = def.layer
|
|
227
|
-
if (def.z !== undefined) node.z = def.z
|
|
228
|
-
if (def.visible !== undefined) node.visible = def.visible
|
|
229
|
-
if (def.locked !== undefined) (node as { _sceneLocked?: boolean })._sceneLocked = def.locked
|
|
230
|
-
if (def.camera !== undefined) (node as { _sceneCamera?: boolean })._sceneCamera = true
|
|
231
|
-
if (def.children) (node as { _sceneChildOrder?: string[] })._sceneChildOrder = Object.keys(def.children)
|
|
232
|
-
if (node instanceof Sprite) {
|
|
233
|
-
if (def.color !== undefined) node.color = def.color
|
|
234
|
-
if (def.opacity !== undefined) node.opacity = def.opacity
|
|
235
|
-
if (def.flipX !== undefined) node.flipX = def.flipX
|
|
236
|
-
if (def.flipY !== undefined) node.flipY = def.flipY
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** The first camera-source def in file order (depth-first), or null. */
|
|
241
|
-
const findCamera = (defs?: Record<string, Scene2dNodeDef>): { name: string, def: Camera2dNodeDef } | null => {
|
|
242
|
-
for (const [ name, nd ] of Object.entries(defs ?? {})) {
|
|
243
|
-
if (nd.camera !== undefined) return { name, def: nd.camera }
|
|
244
|
-
const inner = findCamera(nd.children)
|
|
245
|
-
if (inner) return inner
|
|
246
|
-
}
|
|
247
|
-
return null
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const attachAspects = (
|
|
251
|
-
node: Node2D, def: Scene2dNodeDef, nodes: Record<string, Node2D>, scene: Scene2D, env: Scene2dEnv,
|
|
252
|
-
): void => {
|
|
253
|
-
if (isEditMode()) {
|
|
254
|
-
// Data-only (the future harness reads [ctor, props] from here) — no onAttach side effects
|
|
255
|
-
// (bodies, loops) while editing.
|
|
256
|
-
;(node as unknown as { _sceneAspects: readonly AspectEntry<any>[] })._sceneAspects = def.aspects ?? []
|
|
257
|
-
return
|
|
258
|
-
}
|
|
259
|
-
for (const entry of def.aspects ?? []) {
|
|
260
|
-
let props = resolveRefs(entry.props as Record<string, unknown> | undefined, nodes)
|
|
261
|
-
// Aspects that declare `static wantsScene` (CameraFollow) get the scene wired in, and their
|
|
262
|
-
// pixel snapping defaults to env.pixelPerfect — explicit props always win.
|
|
263
|
-
if ((entry.ctor as { wantsScene?: boolean }).wantsScene) {
|
|
264
|
-
props = {
|
|
265
|
-
scene,
|
|
266
|
-
...(env.pixelPerfect !== undefined ? { pixelSnap: env.pixelPerfect } : {}),
|
|
267
|
-
...props,
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
;(node as Node2D & { aspect(c: unknown, p?: unknown): unknown }).aspect(entry.ctor, props)
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/** Run a def's make() factory (phase 2 — every node exists, so ref() args resolve). The subtree
|
|
275
|
-
* mounts under the wrapper node; the wrapper's transform stays editor-owned. */
|
|
276
|
-
const attachMake = (
|
|
277
|
-
node: Node2D, name: string, def: Scene2dNodeDef, nodes: Record<string, Node2D>, scene: Scene2D,
|
|
278
|
-
): Promise<void> | undefined => {
|
|
279
|
-
const entry = def.make
|
|
280
|
-
if (!entry) return undefined
|
|
281
|
-
const args = resolveRefs({ ...(entry.args ?? {}) } as Record<string, unknown>, nodes) ?? {}
|
|
282
|
-
return Promise.resolve(entry.fn(args as never)).then((made) => {
|
|
283
|
-
if (made instanceof Node2D) {
|
|
284
|
-
node.add(made)
|
|
285
|
-
// membership is per-entity in creator-2d — add the whole made subtree to the draw set
|
|
286
|
-
const addAll = (n: Node2D): void => { scene.add(n); n.children.forEach(addAll) }
|
|
287
|
-
addAll(made)
|
|
288
|
-
}
|
|
289
|
-
}).catch((e) => { console.error(`[scene2d] make() factory failed on "${name}":`, e) })
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/** Build one defs record into `scene` under `parent`, two-phase (all nodes first, then
|
|
293
|
-
* aspects/make — so `ref()`s resolve regardless of declaration order). */
|
|
294
|
-
const buildNodes = async (
|
|
295
|
-
defs: Record<string, Scene2dNodeDef>, parent: Node2D | null, scene: Scene2D,
|
|
296
|
-
nodes: Record<string, Node2D>, env: Scene2dEnv,
|
|
297
|
-
): Promise<void> => {
|
|
298
|
-
const pending: { name: string, node: Node2D, def: Scene2dNodeDef }[] = []
|
|
299
|
-
|
|
300
|
-
const build = async (name: string, nd: Scene2dNodeDef, parentNode: Node2D | null): Promise<void> => {
|
|
301
|
-
const node = await createSource(name, nd)
|
|
302
|
-
if (parentNode) parentNode.add(node)
|
|
303
|
-
scene.add(node)
|
|
304
|
-
applyNode(node, name, nd)
|
|
305
|
-
pending.push({ name, node, def: nd })
|
|
306
|
-
nodes[name] = node
|
|
307
|
-
await Promise.all(Object.entries(nd.children ?? {}).map(([ childName, child ]) => build(childName, child, node)))
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
await Promise.all(Object.entries(defs).map(([ name, nd ]) => build(name, nd, parent)))
|
|
311
|
-
const waits: Promise<void>[] = []
|
|
312
|
-
for (const p of pending) {
|
|
313
|
-
attachAspects(p.node, p.def, nodes, scene, env)
|
|
314
|
-
const wait = attachMake(p.node, p.name, p.def, nodes, scene)
|
|
315
|
-
if (wait) waits.push(wait)
|
|
316
|
-
}
|
|
317
|
-
if (waits.length > 0) await Promise.all(waits)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const instantiate = async (def: Scene2dDef): Promise<{ scene: Scene2D, nodes: Record<string, Node2D> }> => {
|
|
321
|
-
const env = def.env ?? {}
|
|
322
|
-
// configure() resets the physics world, so it must run before any body is created (aspects
|
|
323
|
-
// create bodies right after the node build). Edit mode holds physics as data — skip.
|
|
324
|
-
if (env.gravity !== undefined && !isEditMode()) Physics2D.configure({ gravity: env.gravity })
|
|
325
|
-
|
|
326
|
-
const scene = new Scene2D({ background: env.background, filter: env.filter })
|
|
327
|
-
for (const [ key, layer ] of Object.entries(def.layers ?? {})) {
|
|
328
|
-
if (layer?.ySort) scene.layer(Number(key)).ySort(true)
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const nodes: Record<string, Node2D> = {}
|
|
332
|
-
await buildNodes(def.nodes ?? {}, null, scene, nodes, env)
|
|
333
|
-
|
|
334
|
-
// a camera NODE wins over the top-level `camera:` block; both only SEED the starting view —
|
|
335
|
-
// attach CameraFollow to the tracked node to move the camera in play
|
|
336
|
-
const cam = findCamera(def.nodes)
|
|
337
|
-
const camNode = cam ? nodes[cam.name] : undefined
|
|
338
|
-
if (cam && camNode) {
|
|
339
|
-
const p = camNode.worldPosition
|
|
340
|
-
scene.camera.position = env.pixelPerfect ? [ Math.round(p.x), Math.round(p.y) ] : p
|
|
341
|
-
if (cam.def.zoom !== undefined) scene.camera.zoom = cam.def.zoom
|
|
342
|
-
} else if (def.camera) {
|
|
343
|
-
if (def.camera.position) scene.camera.position = def.camera.position
|
|
344
|
-
if (def.camera.zoom !== undefined) scene.camera.zoom = def.camera.zoom
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return { scene, nodes }
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
export class Scene2dHandle<D extends Scene2dDef = Scene2dDef> {
|
|
351
|
-
readonly def: D
|
|
352
|
-
private _loading?: Promise<LoadedScene2d<D>>
|
|
353
|
-
|
|
354
|
-
constructor(def: D) { this.def = def }
|
|
355
|
-
|
|
356
|
-
/** Instantiate the scene (idempotent — subsequent calls return the same instance). Does not open. */
|
|
357
|
-
load(): Promise<LoadedScene2d<D>> {
|
|
358
|
-
this._loading ??= instantiate(this.def).then((live) => live as LoadedScene2d<D>)
|
|
359
|
-
return this._loading
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/** Load and make active (only the active 2D scene renders). */
|
|
363
|
-
async open(): Promise<LoadedScene2d<D>> {
|
|
364
|
-
const loaded = await this.load()
|
|
365
|
-
loaded.scene.open()
|
|
366
|
-
return loaded
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Define a 2D scene as data — the default export of a `.scene2d.ts` file. Returns a typed handle:
|
|
372
|
-
* `const { scene, nodes } = await handle.open()` gives `nodes.<name>` typed by its source block
|
|
373
|
-
* (Sprite / Tilemap / Node2D) with its `use(...)`d aspects attached.
|
|
374
|
-
*/
|
|
375
|
-
export const defineScene2d = <const D extends Scene2dDef>(def: D): Scene2dHandle<D> => {
|
|
376
|
-
const handle = new Scene2dHandle(def)
|
|
377
|
-
const g = globalThis as unknown as { [EDIT_FLAG]?: boolean, __lecodesScenes2d?: Scene2dHandle[] }
|
|
378
|
-
// Editor hook: expose defined handles to a host that runs the bundle in edit mode.
|
|
379
|
-
if (g[EDIT_FLAG]) (g.__lecodesScenes2d ??= []).push(handle as Scene2dHandle)
|
|
380
|
-
return handle
|
|
381
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// Collision groups — which bodies are allowed to touch which. Called groups, not layers, because
|
|
2
|
-
// `layer` already means DRAW order in 2D (`node.layer`, `scene2d.layer(n).ySort()`).
|
|
3
|
-
//
|
|
4
|
-
// // game/groups.ts
|
|
5
|
-
// export const player = Physics2D.addGroup()
|
|
6
|
-
// export const bullets = Physics2D.addGroup().ignoreSelf().ignore(player)
|
|
7
|
-
//
|
|
8
|
-
// bullet.aspect(Physics2D, { motion: 'dynamic', group: bullets })
|
|
9
|
-
//
|
|
10
|
-
// Two decisions worth knowing, both deliberate:
|
|
11
|
-
//
|
|
12
|
-
// SUBTRACTIVE, not enumerative. A group collides with everything EXCEPT what it ignores, so adding a
|
|
13
|
-
// new group later cannot silently stop existing pairs from colliding. The enumerative form —
|
|
14
|
-
// "collides only with [ground, enemy]" — decays: introduce `crate` a month later and the bullets
|
|
15
|
-
// start flying through it, with nothing to notice.
|
|
16
|
-
//
|
|
17
|
-
// SYMMETRIC. Box2D's filter is an AND of two masks, so declaring the relation on one side only is the
|
|
18
|
-
// classic trap ("A allowed B but B didn't allow A" → they silently pass through). Both sides are set
|
|
19
|
-
// from one undirected edge here, so naming it once is enough.
|
|
20
|
-
//
|
|
21
|
-
// Budget: 32 bits, because a category/mask must cross the bridge as a plain JS number and only 2^32 is
|
|
22
|
-
// exactly representable there. One bit is the implicit default group, one is reserved for the
|
|
23
|
-
// character's shadow body — 30 user groups.
|
|
24
|
-
|
|
25
|
-
/** Collides with everything. */
|
|
26
|
-
export const ALL_GROUPS = 0xffffffff
|
|
27
|
-
|
|
28
|
-
/** @internal Bit 0 — every body that never names a group. */
|
|
29
|
-
export const DEFAULT_BIT = 1 << 0
|
|
30
|
-
/** @internal Bit 1 — the character's shadow body: solid shapes never see it, sensors always do. */
|
|
31
|
-
export const CHARACTER_BIT = 1 << 1
|
|
32
|
-
|
|
33
|
-
const MAX_USER_GROUPS = 30
|
|
34
|
-
let nextBit = 2
|
|
35
|
-
const all: PhysicsGroup2D[] = []
|
|
36
|
-
|
|
37
|
-
// Bodies whose filter has to be re-pushed when the topology changes. Only bodies that actually named
|
|
38
|
-
// a group register, so a project that never touches groups keeps an empty set.
|
|
39
|
-
const listeners = new Set<() => void>()
|
|
40
|
-
/** @internal */
|
|
41
|
-
export const _watchGroupChanges = (fn: () => void): void => { listeners.add(fn) }
|
|
42
|
-
/** @internal */
|
|
43
|
-
export const _unwatchGroupChanges = (fn: () => void): void => { listeners.delete(fn) }
|
|
44
|
-
|
|
45
|
-
export class PhysicsGroup2D {
|
|
46
|
-
/** The single category bit this group owns. */
|
|
47
|
-
readonly bit: number
|
|
48
|
-
/** Optional debug label, so an inspector says "bullets" rather than "group #3". */
|
|
49
|
-
readonly label: string
|
|
50
|
-
|
|
51
|
-
private _ignored = 0
|
|
52
|
-
|
|
53
|
-
/** @internal — use `Physics2D.addGroup()`. */
|
|
54
|
-
constructor(bit: number, label: string) {
|
|
55
|
-
this.bit = bit
|
|
56
|
-
this.label = label
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** @internal The Box2D pair for a shape in this group. */
|
|
60
|
-
get category(): number { return this.bit }
|
|
61
|
-
/** @internal */
|
|
62
|
-
get mask(): number { return (ALL_GROUPS & ~this._ignored) >>> 0 }
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Stop colliding with these groups — symmetric, so naming it on one side is enough. Safe to call
|
|
66
|
-
* after bodies exist: their filters are re-pushed.
|
|
67
|
-
*/
|
|
68
|
-
ignore(...groups: PhysicsGroup2D[]): this {
|
|
69
|
-
for (const g of groups) {
|
|
70
|
-
this._ignored |= g.bit
|
|
71
|
-
g._ignored |= this.bit
|
|
72
|
-
}
|
|
73
|
-
for (const fn of listeners) fn()
|
|
74
|
-
return this
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Members of this group don't collide with EACH OTHER — bullets, debris, a ragdoll's own bones. */
|
|
78
|
-
ignoreSelf(): this {
|
|
79
|
-
this._ignored |= this.bit
|
|
80
|
-
for (const fn of listeners) fn()
|
|
81
|
-
return this
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** @internal The implicit group of every body that never names one. */
|
|
86
|
-
export const defaultGroup = new PhysicsGroup2D(DEFAULT_BIT, "default")
|
|
87
|
-
|
|
88
|
-
/** @internal Register a new group (see `Physics2D.addGroup`). */
|
|
89
|
-
export const _addGroup = (label = ""): PhysicsGroup2D => {
|
|
90
|
-
if (nextBit >= 2 + MAX_USER_GROUPS) {
|
|
91
|
-
throw new Error(`Physics2D.addGroup: out of collision groups (${MAX_USER_GROUPS} max — a category has to cross the bridge as a 32-bit number, and two bits are reserved).`)
|
|
92
|
-
}
|
|
93
|
-
const g = new PhysicsGroup2D((1 << nextBit) >>> 0, label || `group${nextBit - 1}`)
|
|
94
|
-
nextBit++
|
|
95
|
-
all.push(g)
|
|
96
|
-
return g
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** @internal Sensors must always be visible to the character's shadow body, solids never. */
|
|
100
|
-
export const sensorMask = (g: PhysicsGroup2D | undefined): number =>
|
|
101
|
-
(((g ?? defaultGroup).mask | CHARACTER_BIT) & ALL_GROUPS) >>> 0
|
|
102
|
-
/** @internal */
|
|
103
|
-
export const solidMask = (g: PhysicsGroup2D | undefined): number =>
|
|
104
|
-
(((g ?? defaultGroup).mask & ~CHARACTER_BIT) & ALL_GROUPS) >>> 0
|
|
105
|
-
/** @internal */
|
|
106
|
-
export const categoryOf = (g: PhysicsGroup2D | undefined): number => (g ?? defaultGroup).category
|
package/runtime/sdk/g2/loop.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// Internal per-frame plumbing for the 2D engine. NOT user-facing — user game logic uses the host
|
|
2
|
-
// `setLoop` global. `onUpdate` taps the engine's render-synced pre-frame hook (_creator2d.onUpdate),
|
|
3
|
-
// for anything that must move in lockstep with the native render (no 1-frame lag).
|
|
4
|
-
//
|
|
5
|
-
// The animation-event registration is lazy (first use) so a 2D project that never animates a sprite
|
|
6
|
-
// pulls none of this in.
|
|
7
|
-
|
|
8
|
-
import { Vec2 } from "../math/vec"
|
|
9
|
-
import { node2dRegistry } from "./Node2D"
|
|
10
|
-
|
|
11
|
-
export const onUpdate = (callback: (dt: number) => void): void => _creator2d.onUpdate(callback)
|
|
12
|
-
export const offUpdate = (callback: (dt: number) => void): void => _creator2d.offUpdate(callback)
|
|
13
|
-
|
|
14
|
-
// Route native animation events (queued in C++, drained once per frame) to the owning node, which
|
|
15
|
-
// translates the native clip id to a clip name and emits 'loopReached' / 'completed'.
|
|
16
|
-
let animEventsRegistered = false
|
|
17
|
-
export const ensureAnimEvents = (): void => {
|
|
18
|
-
if (animEventsRegistered) return
|
|
19
|
-
animEventsRegistered = true
|
|
20
|
-
_creator2d.setOnAnimEvent(({ entityId, clipId, type }) => {
|
|
21
|
-
node2dRegistry.get(entityId)?._dispatchAnimEvent(clipId, type)
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Route native physics contact/sensor events to the owning nodes' 'enter'/'exit' events. Each event
|
|
26
|
-
// is delivered to BOTH nodes with the other as the argument. type: 0 contactBegin, 1 contactEnd,
|
|
27
|
-
// 2 sensorBegin, 3 sensorEnd. Registered lazily by the first 'enter'/'exit' listener (Node2D) or the
|
|
28
|
-
// first Trigger2D — it used to hang off Trigger2D alone, so solid-vs-solid contacts silently never
|
|
29
|
-
// fired in a project without a single trigger.
|
|
30
|
-
//
|
|
31
|
-
// A contact BEGIN also carries the manifold, which the listener gets as a second argument: the world
|
|
32
|
-
// point, the surface normal, and the approach speed. The normal points from A to B, so it is FLIPPED
|
|
33
|
-
// for the second delivery — each side gets the normal pointing away from the thing it hit, which is
|
|
34
|
-
// what "did I land on top or hit a wall" needs.
|
|
35
|
-
let physicsEventsRegistered = false
|
|
36
|
-
export const ensurePhysicsEvents = (): void => {
|
|
37
|
-
if (physicsEventsRegistered) return
|
|
38
|
-
physicsEventsRegistered = true
|
|
39
|
-
_creator2d.setOnPhysicsEvent((a, b, type, px, py, nx, ny, speed) => {
|
|
40
|
-
const na = node2dRegistry.get(a)
|
|
41
|
-
const nb = node2dRegistry.get(b)
|
|
42
|
-
if (!na || !nb) return
|
|
43
|
-
const channel = type === 1 || type === 3 ? "exit" : "enter"
|
|
44
|
-
// Box2D's manifold normal points from A TOWARD B. Each listener wants the normal of the surface
|
|
45
|
-
// it hit, pointing back at itself — so A gets it negated and B gets it as-is. Landing on a floor
|
|
46
|
-
// then reads normal.y ≈ +1 for the falling body, which is the test a game actually writes.
|
|
47
|
-
na._emitCollision(channel, nb, { point: new Vec2(px, py), normal: new Vec2(-nx, -ny), speed })
|
|
48
|
-
nb._emitCollision(channel, na, { point: new Vec2(px, py), normal: new Vec2(nx, ny), speed })
|
|
49
|
-
})
|
|
50
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// No-code 2D scenario aspects (the gl/scenarios.ts precedent): behaviors a scene builder attaches
|
|
2
|
-
// in a `.scene2d.ts` file without writing aspect code. First resident: CameraFollow — the eased,
|
|
3
|
-
// pixel-snapped follow camera every top-down/side-scroller needs (it absorbs the reference app's
|
|
4
|
-
// ~50 lines of snap/follow plumbing).
|
|
5
|
-
//
|
|
6
|
-
// Conventions: update() runs in the LATE phase (after physics, render-synced when the engine
|
|
7
|
-
// provides the hooks), so followers read final, drawn-this-frame positions.
|
|
8
|
-
|
|
9
|
-
import { Aspect } from "../core/Aspect"
|
|
10
|
-
import type { Camera2D } from "./Camera2D"
|
|
11
|
-
import type { Node2D } from "./Node2D"
|
|
12
|
-
import type { Scene2D } from "./Scene2D"
|
|
13
|
-
|
|
14
|
-
// SCREEN pixels per world unit, MEASURED from the native projection (zoom + world→screen mapping
|
|
15
|
-
// are applied natively, so "1 world unit = 1 px" can't be assumed — and worldToScreen is affine,
|
|
16
|
-
// so the per-unit delta is position-independent). Re-measured only when zoom changes.
|
|
17
|
-
type PixelGrid = { sx: number, sy: number, zoomAt: number }
|
|
18
|
-
|
|
19
|
-
const snapToGrid = (cam: Camera2D, grid: PixelGrid, x: number, y: number): [number, number] => {
|
|
20
|
-
if (grid.sx === 0 || cam.zoom !== grid.zoomAt) {
|
|
21
|
-
const o = cam.worldToScreen(0, 0)
|
|
22
|
-
grid.sx = Math.abs(cam.worldToScreen(1, 0).x - o.x) || 1
|
|
23
|
-
grid.sy = Math.abs(cam.worldToScreen(0, 1).y - o.y) || 1
|
|
24
|
-
grid.zoomAt = cam.zoom
|
|
25
|
-
}
|
|
26
|
-
return [ Math.round(x * grid.sx) / grid.sx, Math.round(y * grid.sy) / grid.sy ]
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Ease the scene camera toward this node every frame (attach it to the player). In a `.scene2d.ts`
|
|
31
|
-
* file the loader wires `scene` automatically; from hand-written code pass it yourself:
|
|
32
|
-
* `hero.aspect(CameraFollow, { scene })`.
|
|
33
|
-
*
|
|
34
|
-
* The eased position accumulates at full precision and only the value PUSHED to the camera is
|
|
35
|
-
* snapped — reading the snapped camera back as the ease-from value would let tiny sub-pixel steps
|
|
36
|
-
* round to zero near settle (the follow would stall a few px off-centre, then jump).
|
|
37
|
-
*/
|
|
38
|
-
export class CameraFollow extends Aspect<"cameraFollow", Node2D> {
|
|
39
|
-
static readonly aspect = "cameraFollow"
|
|
40
|
-
/** The scene2d loader injects the scene into aspects that declare this. */
|
|
41
|
-
static readonly wantsScene = true
|
|
42
|
-
|
|
43
|
-
/** The scene whose camera follows (wired by the scene file loader). */
|
|
44
|
-
scene: Scene2D | null = null
|
|
45
|
-
/** Exponential time constant in seconds; smaller = snappier, 0 = pin the node to centre. */
|
|
46
|
-
smooth = 0.12
|
|
47
|
-
/** Camera offset from the node, in world units. */
|
|
48
|
-
offset: [number, number] = [ 0, 0 ]
|
|
49
|
-
/** Quantize the camera to whole screen pixels so eased motion never shimmers pixel art.
|
|
50
|
-
* Defaults to the scene's `env.pixelPerfect` (true when unset). */
|
|
51
|
-
pixelSnap = true
|
|
52
|
-
|
|
53
|
-
private _x = NaN
|
|
54
|
-
private _y = NaN
|
|
55
|
-
private readonly _grid: PixelGrid = { sx: 0, sy: 0, zoomAt: NaN }
|
|
56
|
-
|
|
57
|
-
update(dt: number): void {
|
|
58
|
-
const scene = this.scene
|
|
59
|
-
if (!scene) return
|
|
60
|
-
const cam = scene.camera
|
|
61
|
-
const p = this.node.worldPosition
|
|
62
|
-
const tx = p.x + this.offset[0], ty = p.y + this.offset[1]
|
|
63
|
-
if (Number.isNaN(this._x)) { const c = cam.position; this._x = c.x; this._y = c.y } // seed from the camera's pose
|
|
64
|
-
const k = this.smooth > 0 ? 1 - Math.exp(-dt / this.smooth) : 1
|
|
65
|
-
this._x += (tx - this._x) * k
|
|
66
|
-
this._y += (ty - this._y) * k
|
|
67
|
-
cam.position = this.pixelSnap ? snapToGrid(cam, this._grid, this._x, this._y) : [ this._x, this._y ]
|
|
68
|
-
}
|
|
69
|
-
}
|