lecodes-sdk 2.4.9 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/global.d.ts +35 -0
  2. package/dist/types/core/env.d.ts +25 -0
  3. package/dist/types/core/particlesConfig.d.ts +196 -0
  4. package/dist/types/g2/Font2D.d.ts +19 -0
  5. package/dist/types/g2/Material2D.d.ts +24 -0
  6. package/dist/types/g2/Node2D.d.ts +6 -5
  7. package/dist/types/g2/Particles2D.d.ts +169 -0
  8. package/dist/types/g2/Path2D.d.ts +95 -0
  9. package/dist/types/g2/Physics2D.d.ts +1 -1
  10. package/dist/types/g2/Scene2D.d.ts +15 -17
  11. package/dist/types/g2/Sprite.d.ts +12 -1
  12. package/dist/types/g2/SpriteSheet.d.ts +4 -1
  13. package/dist/types/g2/Text2D.d.ts +96 -0
  14. package/dist/types/g2/Tilemap.d.ts +8 -0
  15. package/dist/types/g2/defineScene2d.d.ts +69 -11
  16. package/dist/types/gl/Foliage.d.ts +0 -7
  17. package/dist/types/gl/Lightmap.d.ts +101 -2
  18. package/dist/types/gl/Material.d.ts +21 -15
  19. package/dist/types/gl/Model.d.ts +8 -3
  20. package/dist/types/gl/Particles.d.ts +3 -135
  21. package/dist/types/gl/Statics.d.ts +56 -0
  22. package/dist/types/gl/staticsFormat.d.ts +101 -0
  23. package/dist/types/inject.d.ts +11 -3
  24. package/dist/types/runtime/env.d.ts +10 -0
  25. package/dist/types/runtime/envValues.d.ts +8 -0
  26. package/dist/types/scene/defineScene.d.ts +17 -12
  27. package/dist/types/server/env.d.ts +14 -0
  28. package/dist/types/server/inject.d.ts +1 -0
  29. package/dist/types/ui/fonts.d.ts +4 -0
  30. package/dist/types/version.d.ts +1 -1
  31. package/dist/types.json +1 -1
  32. package/package.json +1 -1
  33. package/src/bridges/2d.d.ts +65 -5
  34. package/src/bridges/gl.d.ts +34 -15
  35. package/src/compile/bakeMacro.ts +3 -0
  36. package/src/compile/compileProject.ts +25 -4
  37. package/src/compile/envCompile.ts +74 -0
  38. package/src/compile/fontMacro.ts +11 -6
  39. package/src/compile/index.ts +6 -0
  40. package/src/compile/resourceSlots.ts +1 -0
  41. package/src/compile/serverSplit.ts +47 -4
  42. package/src/core/env.ts +47 -0
  43. package/src/core/particlesConfig.ts +422 -0
  44. package/src/g2/Font2D.ts +87 -0
  45. package/src/g2/Material2D.ts +64 -0
  46. package/src/g2/Node2D.ts +10 -6
  47. package/src/g2/Particles2D.ts +337 -0
  48. package/src/g2/Path2D.ts +215 -0
  49. package/src/g2/Physics2D.ts +1 -1
  50. package/src/g2/Scene2D.ts +16 -22
  51. package/src/g2/Sprite.ts +23 -4
  52. package/src/g2/SpriteSheet.ts +9 -5
  53. package/src/g2/Text2D.ts +178 -0
  54. package/src/g2/Tilemap.ts +10 -0
  55. package/src/g2/defineScene2d.ts +108 -21
  56. package/src/g2/groups2d.ts +2 -2
  57. package/src/gl/Foliage.ts +1 -16
  58. package/src/gl/Lightmap.ts +79 -26
  59. package/src/gl/Material.ts +51 -21
  60. package/src/gl/Model.ts +19 -5
  61. package/src/gl/Particles.ts +22 -361
  62. package/src/gl/Statics.ts +301 -0
  63. package/src/gl/nav/NavMesh.ts +4 -1
  64. package/src/gl/staticsFormat.ts +240 -0
  65. package/src/inject.ts +18 -3
  66. package/src/runtime/env.ts +13 -0
  67. package/src/runtime/envValues.ts +8 -0
  68. package/src/runtime/fetch.ts +18 -0
  69. package/src/scene/defineScene.ts +107 -27
  70. package/src/server/env.ts +25 -0
  71. package/src/server/host.ts +6 -0
  72. package/src/server/inject.ts +2 -1
  73. package/src/ui/fonts.ts +46 -0
  74. package/src/version.ts +1 -1
package/src/g2/Sprite.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  // A textured 2D node. Cheap setters proxy straight to the native sprite; the data lives in
2
2
  // creator-2d. Sprite-sheet animation is no longer built in — attach the SpriteAnimation aspect:
3
3
  // new Sprite({ texture }).aspect(SpriteAnimation, { size: [32, 48], idle: [0,1,2,3] }).anim.play('idle')
4
+ // Without a texture a sprite is a solid rect: it draws the engine's 1x1 white texture, so `color` is
5
+ // the rect's colour and `size` its world size — new Sprite({ size: [200, 20], color: '#3a7' }).
4
6
 
5
7
  import { Color, type ColorInput } from "../core/color"
6
8
  import { Vec2, type Vec2Like } from "../math/vec"
7
9
  import { Canvas, isCanvas } from "../canvas/Canvas"
8
10
  import { Node2D, type Node2DTweenProps } from "./Node2D"
9
11
  import type { Texture2D } from "./Texture2D"
12
+ import type { Material2D } from "./Material2D"
10
13
  import { DOM_SPRITE2D, colorValue, floatValue, type TweenChannel, type TweenMeta } from "../animate/tween/spec"
11
14
  import type { Animation } from "../animate/tween/Animation"
12
15
 
@@ -20,6 +23,7 @@ export type SpriteTweenProps = Node2DTweenProps & {
20
23
  export type TextureSource = Texture2D | Canvas
21
24
 
22
25
  export type SpriteOptions = {
26
+ /** Omit the texture for a solid rect of `color` with `size`. */
23
27
  texture?: TextureSource
24
28
  /** Pivot, [0,0] = top-left .. [1,1] = bottom-right. Use [0.5, 1] (feet) for Y-sorted depth. */
25
29
  anchor?: Vec2Like
@@ -30,7 +34,10 @@ export type SpriteOptions = {
30
34
  /** Tint multiplied with the texture. */
31
35
  color?: ColorInput
32
36
  opacity?: number
33
- layer?: number
37
+ /** Coarse draw order 0..7, default 4 — see {@link Node2D.renderPriority}. */
38
+ renderPriority?: number
39
+ /** Blend + sampling — see {@link Material2D}. Default `Material2D.default`. */
40
+ material?: Material2D
34
41
  position?: Vec2Like
35
42
  }
36
43
 
@@ -42,25 +49,32 @@ export class Sprite extends Node2D {
42
49
  private _flipX = false
43
50
  private _flipY = false
44
51
  private _size: Vec2 | null = null
52
+ private _material?: Material2D
45
53
 
46
54
  constructor(options: SpriteOptions = {}) {
47
55
  super()
56
+ // The texture first (its setSprite resets the native size to the texture's), then the size.
57
+ // No texture but a size / colour = a solid rect (the white texture). No texture and neither = a
58
+ // sprite whose texture comes later (a SpriteSheet entry still loading): nothing to draw yet.
48
59
  if (options.texture) this.texture = options.texture
60
+ else if (options.size || options.color !== undefined) this.texture = undefined
49
61
  if (options.size) this.size = options.size
50
62
  if (options.anchor) this.anchor = options.anchor
51
63
  if (options.frame) this.frame = options.frame
52
64
  if (options.color !== undefined) this.color = options.color
53
65
  if (options.opacity !== undefined) this.opacity = options.opacity
54
- if (options.layer !== undefined) this.layer = options.layer
66
+ if (options.renderPriority !== undefined) this.renderPriority = options.renderPriority
67
+ if (options.material) this.material = options.material
55
68
  if (options.position) this.position = options.position
56
69
  }
57
70
 
71
+ /** The art; undefined = a solid rect of `color` (the engine's white texture). */
58
72
  get texture(): Texture2D | undefined { return this._texture }
59
73
  set texture(t: TextureSource | undefined) {
60
74
  const tex = isCanvas(t) ? t.texture() : t // a Canvas bakes to a texture on assign
61
75
  this._texture = tex
62
- if (!tex) return
63
- _creator2d.setSprite(this.id, tex.id) // native resets UV + size to the full (pixel) texture
76
+ // native resets UV + size to the full (pixel) texture — 1x1 for the white one
77
+ _creator2d.setSprite(this.id, tex ? tex.id : _creator2d.whiteTexture())
64
78
  // A Canvas is authored in LOGICAL units but baked at logical × pixelRatio, so default the sprite's
65
79
  // WORLD size to the canvas's LOGICAL size — pixelRatio then only affects crispness, not on-screen
66
80
  // size. (A plain texture keeps the native default: the texture's pixel dimensions.)
@@ -101,6 +115,11 @@ export class Sprite extends Node2D {
101
115
  get opacity(): number { return this._opacity }
102
116
  set opacity(a: number) { this._opacity = a; _creator2d.setSpriteOpacity(this.id, a) }
103
117
 
118
+ /** Blend + sampling ({@link Material2D}); undefined = the default (alpha, renderer filter, clamp).
119
+ * A material is data: assign a new one to change it. */
120
+ get material(): Material2D | undefined { return this._material }
121
+ set material(m: Material2D) { this._material = m; m._apply(this.id) }
122
+
104
123
  get flipX(): boolean { return this._flipX }
105
124
  set flipX(v: boolean) { this._flipX = v; _creator2d.setSpriteFlip(this.id, v, this._flipY) }
106
125
  get flipY(): boolean { return this._flipY }
@@ -5,9 +5,9 @@
5
5
  //
6
6
  // import props from './props.sprite'
7
7
  // import hero from './hero.sprite'
8
- // scene.add(props.make('tree_orange', { position: [80, 144], layer: 1 }))
9
- // scene.add(props.make('water', { position: [0, 0] })) // sliced, loops on its own
10
- // const player = hero.make('hero', { clip: 'idle', layer: 1 })
8
+ // scene.add(props.make('tree_orange', { position: [80, 144] }))
9
+ // scene.add(props.make('water', { position: [0, 0], z: -1 })) // sliced, loops on its own
10
+ // const player = hero.make('hero', { clip: 'idle' })
11
11
  // player.anim.play('walk', inputVector)
12
12
  //
13
13
  // A sheet describes ART only — rects, slices, clips, anchors. World semantics (colliders, bodies)
@@ -57,7 +57,10 @@ export type SpriteSheetDef = {
57
57
 
58
58
  export type SpriteMakeOptions = {
59
59
  position?: Vec2Like
60
- layer?: number
60
+ /** Coarse draw order 0..7, default 4 — see {@link Node2D.renderPriority}. */
61
+ renderPriority?: number
62
+ /** Draw order within a renderPriority — see {@link Node2D.z}. */
63
+ z?: number
61
64
  /** Overrides the sheet/sprite anchor. */
62
65
  anchor?: Vec2Like
63
66
  /** Start this clip immediately (sliced sprites with clips). */
@@ -109,9 +112,10 @@ export class SpriteSheet {
109
112
 
110
113
  const sprite = new Sprite({
111
114
  anchor: opts.anchor ?? named.anchor ?? def.anchor,
112
- layer: opts.layer,
115
+ renderPriority: opts.renderPriority,
113
116
  position: opts.position,
114
117
  })
118
+ if (opts.z !== undefined) sprite.z = opts.z
115
119
 
116
120
  // The world size is known without the texture (the rect, or its slice cell) — set it up front
117
121
  // so layout, Y-sort and colliders are correct even while the image is still loading.
@@ -0,0 +1,178 @@
1
+ // Text in a 2D scene: signed-distance-field glyphs drawn in the sprite batch, so a text sorts by
2
+ // renderPriority / z (and ySort) like any sprite and batches with them.
3
+ //
4
+ // const inter = await Font2D.load(font("inter"))
5
+ // new Text2D({ font: inter, text: 'Score 0', fontSize: 24, position: [0, 120] })
6
+ // new Text2D({ font: inter, text: 'Moscow', fontSize: 13, units: 'px', anchor: [0.5, 1],
7
+ // outline: { width: 0.12, color: '#000' } }) // a map label
8
+ //
9
+ // One atlas per font serves every size, and SDF edges stay sharp when the camera zooms in. `units:
10
+ // 'px'` sizes the text in logical screen pixels, constant while the camera zooms — map labels,
11
+ // nameplates; the default 'world' scales with the scene. `outline` is a halo around the glyphs
12
+ // (width in em), what keeps a label readable over any background.
13
+ //
14
+ // Scope: Latin / Cyrillic / Greek-style scripts with kerning, '\n' and width wrapping, left / centre /
15
+ // right alignment. No complex shaping (Arabic, Indic) and no emoji — UI text (UIText) does those.
16
+ //
17
+ // Layout is native: the block is laid out once per `text` / `font` / `fontSize` / `units` / style write,
18
+ // never per frame — animate the node's transform, not its text, when something only moves.
19
+
20
+ import { Color, type ColorInput } from "../core/color"
21
+ import { Vec2, type Vec2Like } from "../math/vec"
22
+ import { Node2D } from "./Node2D"
23
+ import type { Font2D } from "./Font2D"
24
+ import type { Material2D } from "./Material2D"
25
+
26
+ export type TextAlign2D = "left" | "center" | "right"
27
+
28
+ export type TextOutline2D = {
29
+ /** Halo width in em: 0.05 thin … 0.15 the maximum. 0 = none. */
30
+ width: number
31
+ /** Default black. */
32
+ color?: ColorInput
33
+ }
34
+
35
+ export type Text2DOptions = {
36
+ font: Font2D
37
+ text: string
38
+ /** Em height. `units: 'world'` (default) world units; `'px'` logical screen px — constant while the
39
+ * camera zooms (map labels, nameplates). Default 16. */
40
+ fontSize?: number
41
+ units?: "world" | "px"
42
+ /** Default white. */
43
+ color?: ColorInput
44
+ /** Line alignment inside the block; default 'left'. */
45
+ align?: TextAlign2D
46
+ /** Wrap width in fontSize units; omit = no wrap. '\n' always breaks. */
47
+ maxWidth?: number
48
+ /** Multiplier of the font's own line height (default: the font's). */
49
+ lineHeight?: number
50
+ /** Extra spacing between letters, in em. Default 0. */
51
+ letterSpacing?: number
52
+ /** A halo around the glyphs; omit = none. */
53
+ outline?: TextOutline2D
54
+ /** Block anchor, [0,0] top-left … [1,1] bottom-right; default [0.5, 0.5] (centred on the node). */
55
+ anchor?: Vec2Like
56
+ position?: Vec2Like
57
+ /** Degrees, CCW. */
58
+ rotation?: number
59
+ scale?: Vec2Like | number
60
+ /** Draw order within a renderPriority — see {@link Node2D.z}. */
61
+ z?: number
62
+ /** Coarse draw order 0..7, default 4 — see {@link Node2D.renderPriority}. */
63
+ renderPriority?: number
64
+ /** Only `blend` matters for a text; see Material2D. */
65
+ material?: Material2D
66
+ }
67
+
68
+ const ALIGNS: Record<TextAlign2D, number> = { left: 0, center: 1, right: 2 }
69
+
70
+ export class Text2D extends Node2D {
71
+ private _font: Font2D
72
+ private _text: string
73
+ private _fontSize = 16
74
+ private _units: "world" | "px" = "world"
75
+ private _color: ColorInput = 0xffffff
76
+ private _align: TextAlign2D = "left"
77
+ private _maxWidth?: number
78
+ private _lineHeight?: number
79
+ private _letterSpacing = 0
80
+ private _outline: TextOutline2D | null = null
81
+ private _anchor = new Vec2(0.5, 0.5)
82
+ private _material?: Material2D
83
+
84
+ constructor(options: Text2DOptions) {
85
+ super()
86
+ this._font = options.font
87
+ this._text = options.text
88
+ if (options.fontSize !== undefined) this._fontSize = options.fontSize
89
+ if (options.units !== undefined) this._units = options.units
90
+ if (options.align !== undefined) this._align = options.align
91
+ this._maxWidth = options.maxWidth
92
+ this._lineHeight = options.lineHeight
93
+ if (options.letterSpacing !== undefined) this._letterSpacing = options.letterSpacing
94
+ // setText first: it is what makes the entity a text natively; style / colour / anchor come after.
95
+ this._sendText()
96
+ this._sendStyle()
97
+ this.color = options.color ?? this._color
98
+ if (options.outline) this.outline = options.outline
99
+ this.anchor = options.anchor ?? this._anchor
100
+ if (options.material) this.material = options.material
101
+ if (options.position) this.position = options.position
102
+ if (options.rotation !== undefined) this.rotation = options.rotation
103
+ if (options.scale !== undefined) this.scale = options.scale
104
+ if (options.z !== undefined) this.z = options.z
105
+ if (options.renderPriority !== undefined) this.renderPriority = options.renderPriority
106
+ }
107
+
108
+ // One call carries text + font + size + units: any of them re-lays out the block.
109
+ private _sendText(): void {
110
+ _creator2d.setText(this.id, this._font.id, this._text, this._fontSize, this._units === "px")
111
+ }
112
+
113
+ private _sendStyle(): void {
114
+ _creator2d.setTextStyle(
115
+ this.id, ALIGNS[this._align], this._maxWidth ?? 0, this._lineHeight ?? 0, this._letterSpacing,
116
+ )
117
+ }
118
+
119
+ /** The string; '\n' breaks a line. A write re-lays out the block natively. */
120
+ get text(): string { return this._text }
121
+ set text(v: string) { this._text = v; this._sendText() }
122
+
123
+ get font(): Font2D { return this._font }
124
+ set font(f: Font2D) { this._font = f; this._sendText() }
125
+
126
+ /** Em height, in `units`. */
127
+ get fontSize(): number { return this._fontSize }
128
+ set fontSize(v: number) { this._fontSize = v; this._sendText() }
129
+
130
+ /** 'world' (default) scales with the camera; 'px' = logical screen px, constant while zooming. */
131
+ get units(): "world" | "px" { return this._units }
132
+ set units(v: "world" | "px") { this._units = v; this._sendText() }
133
+
134
+ get color(): ColorInput { return this._color }
135
+ set color(c: ColorInput) {
136
+ this._color = c
137
+ const [ r, g, b, a ] = Color.toRgba01(c)
138
+ _creator2d.setTextColor(this.id, r, g, b, a)
139
+ }
140
+
141
+ get align(): TextAlign2D { return this._align }
142
+ set align(v: TextAlign2D) { this._align = v; this._sendStyle() }
143
+
144
+ /** Wrap width in fontSize units; undefined = no wrap. */
145
+ get maxWidth(): number | undefined { return this._maxWidth }
146
+ set maxWidth(v: number | undefined) { this._maxWidth = v; this._sendStyle() }
147
+
148
+ /** Multiplier of the font's own line height; undefined = the font's. */
149
+ get lineHeight(): number | undefined { return this._lineHeight }
150
+ set lineHeight(v: number | undefined) { this._lineHeight = v; this._sendStyle() }
151
+
152
+ /** Extra spacing between letters, in em. */
153
+ get letterSpacing(): number { return this._letterSpacing }
154
+ set letterSpacing(v: number) { this._letterSpacing = v; this._sendStyle() }
155
+
156
+ /** The halo, or null = none. */
157
+ get outline(): TextOutline2D | null { return this._outline ? { ...this._outline } : null }
158
+ set outline(o: TextOutline2D | null) {
159
+ this._outline = o ? { ...o } : null
160
+ if (!o || o.width <= 0) { _creator2d.setTextOutline(this.id, 0, 0, 0, 0, 0); return }
161
+ const [ r, g, b, a ] = o.color !== undefined ? Color.toRgba01(o.color) : [ 0, 0, 0, 1 ]
162
+ _creator2d.setTextOutline(this.id, o.width, r, g, b, a)
163
+ }
164
+
165
+ /** Block anchor, [0,0] top-left … [1,1] bottom-right. */
166
+ get anchor(): Vec2 { return this._anchor.clone() }
167
+ set anchor(v: Vec2Like) { this._anchor = new Vec2(v); _creator2d.setSpriteAnchor(this.id, this._anchor.x, this._anchor.y) }
168
+
169
+ /** Blend ({@link Material2D}) — only `blend` matters for a text. Undefined = the default (alpha). */
170
+ get material(): Material2D | undefined { return this._material }
171
+ set material(m: Material2D) { this._material = m; m._apply(this.id) }
172
+
173
+ /** The laid-out block [width, height] in fontSize units (from the engine). */
174
+ get bounds(): Vec2 {
175
+ const s = _creator2d.getTextSize(this.id)
176
+ return new Vec2(s[0]!, s[1]!)
177
+ }
178
+ }
package/src/g2/Tilemap.ts CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { Node2D } from "./Node2D"
5
5
  import type { Texture2D } from "./Texture2D"
6
+ import type { Material2D } from "./Material2D"
6
7
 
7
8
  export type TilemapOptions = {
8
9
  texture: Texture2D
@@ -15,11 +16,14 @@ export type TilemapOptions = {
15
16
  atlas: [number, number]
16
17
  /** Row-major tile indices into the atlas (-1 = empty), length cols*rows. Row 0 is the top row. */
17
18
  data: number[] | Int32Array
19
+ /** Blend + sampling — see {@link Material2D}. Default `Material2D.default`. */
20
+ material?: Material2D
18
21
  }
19
22
 
20
23
  export class Tilemap extends Node2D {
21
24
  readonly cols: number
22
25
  readonly rows: number
26
+ private _material?: Material2D
23
27
 
24
28
  constructor(options: TilemapOptions) {
25
29
  const data = options.data instanceof Int32Array ? options.data : Int32Array.from(options.data)
@@ -32,8 +36,14 @@ export class Tilemap extends Node2D {
32
36
  super(id) // wrap the native tilemap entity
33
37
  this.cols = options.cols
34
38
  this.rows = options.rows
39
+ if (options.material) this.material = options.material
35
40
  }
36
41
 
42
+ /** Blend + sampling ({@link Material2D}); undefined = the default (alpha, renderer filter, clamp).
43
+ * A material is data: assign a new one to change it. */
44
+ get material(): Material2D | undefined { return this._material }
45
+ set material(m: Material2D) { this._material = m; m._apply(this.id) }
46
+
37
47
  setTile(x: number, y: number, index: number): this {
38
48
  _creator2d.setTile(this.id, x, y, index)
39
49
  return this
@@ -2,19 +2,24 @@
2
2
  // The 2D sibling of scene/defineScene.ts — same literal grammar (one default-exported
3
3
  // `defineScene2d({...})` call, nodes keyed by name, `use(...)` aspects, `make(...)` factories),
4
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.
5
+ // `tilemap` (texture + grid + a `cells('…')` payload), `path` (vector lines / polygons — Path2D),
6
+ // `text` (SDF text — Text2D; `font` is a font("…") family or a font file URL), `particles` (a 2D
7
+ // particle system — Particles2D; `texture` is an asset URL), `camera`, `make`, or none = group.
6
8
  //
7
9
  // // forest.scene2d.ts
8
10
  // import props from './props.sprite'
9
11
  // import hero from './hero.sprite'
10
12
  // export default defineScene2d({
11
- // env: { background: '#5a6b3b', pixelPerfect: true, gravity: [0, 0] },
12
- // layers: { 1: { ySort: true } },
13
+ // env: { background: '#5a6b3b', pixelPerfect: true, gravity: [0, 0], ySort: true },
13
14
  // nodes: {
14
15
  // 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,
16
+ // cells: cells('…') }, z: -1 }, // under everything Y-sorted at z 0
17
+ // tree1: { sprite: { sheet: props, name: 'tree_orange' }, position: [160, 288] },
18
+ // road: { path: { points: [[0, 0], [400, 120]], stroke: { width: 3, units: 'px' } } },
19
+ // sign: { text: { font: font('inter'), text: 'Forest', fontSize: 14, units: 'px' }, position: [0, 64] },
20
+ // fire: { particles: { rate: 40, lifetime: 0.8, size: 16, startVelocity: [0, 90], color: '#ffaa33' },
21
+ // material: { blend: 'additive' }, position: [320, 96] },
22
+ // player: { sprite: { sheet: hero, clip: 'idle' }, position: [800, 200],
18
23
  // aspects: [use(CameraFollow, { smooth: 0.12 })] },
19
24
  // },
20
25
  // })
@@ -38,6 +43,11 @@ import { Scene2D } from "./Scene2D"
38
43
  import { Node2D } from "./Node2D"
39
44
  import { Sprite } from "./Sprite"
40
45
  import { Tilemap } from "./Tilemap"
46
+ import { Path2D, type Stroke2D } from "./Path2D"
47
+ import { Text2D, type TextAlign2D, type TextOutline2D } from "./Text2D"
48
+ import { Particles2D, type Particles2DOptions } from "./Particles2D"
49
+ import { Font2D } from "./Font2D"
50
+ import { Material2D, type Material2DOptions } from "./Material2D"
41
51
  import { Texture2D } from "./Texture2D"
42
52
  import { Physics2D } from "./Physics2D"
43
53
  import { SpriteSheet } from "./SpriteSheet"
@@ -51,8 +61,12 @@ import type { With } from "../core/Aspect"
51
61
  export type Scene2dEnv = {
52
62
  /** Background color — '#5a6b3b', 0x5a6b3b, or [r,g,b] in 0..1. */
53
63
  background?: ColorInput
54
- /** Texture sampling: 'nearest' (crisp pixel art, engine default) or 'linear'. */
64
+ /** Default texture sampling: 'nearest' (crisp pixel art, engine default) or 'linear'. A node's
65
+ * `material.filter` overrides it. */
55
66
  filter?: "nearest" | "linear"
67
+ /** Top-down depth: among nodes of one renderPriority and one z, the one lower on screen draws on
68
+ * top (see Scene2D.ySort). */
69
+ ySort?: boolean
56
70
  /** Pixel-art crispness: snap the camera to whole screen pixels (seeds CameraFollow's
57
71
  * `pixelSnap`, and the initial camera position lands on whole world units). */
58
72
  pixelPerfect?: boolean
@@ -85,6 +99,51 @@ export type TilemapSourceDef = {
85
99
  cells: CellsData | { cols: number, rows: number, data: number[] | Int32Array }
86
100
  }
87
101
 
102
+ /** A vector node (Path2D): a polyline / polygon in the node's local frame. */
103
+ export type PathSourceDef = {
104
+ points: readonly Vec2Like[]
105
+ /** Close the stroke's ring (fills always close). Default false. */
106
+ closed?: boolean
107
+ /** A number = `{ width }`. Omit = no stroke. */
108
+ stroke?: Stroke2D | number
109
+ /** Omit = no fill. */
110
+ fill?: ColorInput
111
+ /** Hole rings — fills only. */
112
+ holes?: readonly (readonly Vec2Like[])[]
113
+ /** Edge anti-aliasing (default true). */
114
+ antialias?: boolean
115
+ }
116
+
117
+ /** A text node (Text2D). `font` is a STRING: a family registered by `font("…")` (what the macro
118
+ * compiles to, e.g. "Inter") or a font file URL — `asset('./fonts/Brand.ttf')`. */
119
+ export type TextSourceDef = {
120
+ font: string
121
+ text: string
122
+ /** Em height (default 16), in `units`. */
123
+ fontSize?: number
124
+ /** 'world' (default) or 'px' — logical screen px, constant while the camera zooms. */
125
+ units?: "world" | "px"
126
+ /** Default white. */
127
+ color?: ColorInput
128
+ align?: TextAlign2D
129
+ /** Wrap width in fontSize units; omit = no wrap. */
130
+ maxWidth?: number
131
+ /** Multiplier of the font's own line height. */
132
+ lineHeight?: number
133
+ /** Extra spacing in em. */
134
+ letterSpacing?: number
135
+ outline?: TextOutline2D
136
+ /** Block anchor, [0,0] top-left … [1,1] bottom-right; default [0.5, 0.5]. */
137
+ anchor?: Vec2Like
138
+ }
139
+
140
+ /** A particle system node (Particles2D): the Particles2D options, with `texture` as a STRING — the
141
+ * sheet image's URL (`asset('./spark.png')`), loaded with Texture2D.load. The blend is the node's
142
+ * generic `material`; position / z / renderPriority are the node's too. */
143
+ export type Particles2DSourceDef = Omit<Particles2DOptions, "texture" | "material" | "position" | "z" | "renderPriority"> & {
144
+ texture?: string
145
+ }
146
+
88
147
  /** A camera marker node: seeds the scene camera's starting position (+ zoom). To MOVE the camera
89
148
  * in play, attach `CameraFollow` to the node being followed — not aspects on this marker. */
90
149
  export type Camera2dNodeDef = { zoom?: number }
@@ -93,6 +152,9 @@ export type Scene2dNodeDef = {
93
152
  // -- source (at most one; none = plain group node) --
94
153
  sprite?: SpriteSourceDef
95
154
  tilemap?: TilemapSourceDef
155
+ path?: PathSourceDef
156
+ text?: TextSourceDef
157
+ particles?: Particles2DSourceDef
96
158
  camera?: Camera2dNodeDef
97
159
  /** A code-built subtree — `make(factoryFn, { ...literal args })`, factory returns a Node2D. */
98
160
  make?: SharedMakeEntry<any, Node2D>
@@ -101,11 +163,14 @@ export type Scene2dNodeDef = {
101
163
  /** Degrees, CCW. */
102
164
  rotation?: number
103
165
  scale?: Vec2Like | number
104
- /** Draw layer (int) — higher renders on top. */
105
- layer?: number
106
- /** Intra-layer depth for non-Y-sorted layers. */
166
+ /** Coarse draw order 0 (first) … 7 (last), default 4 — see Node2D.renderPriority. */
167
+ renderPriority?: number
168
+ /** Draw order within a renderPriority (higher = on top); with `env.ySort`, equal z sorts by Y. */
107
169
  z?: number
108
- /** Sprite tint (sprite nodes only). */
170
+ /** Blend + sampling (sprite, tilemap, text and particles nodes; only `blend` matters for a text) —
171
+ * see Material2D. */
172
+ material?: Material2DOptions
173
+ /** Sprite tint / text colour (sprite and text nodes). */
109
174
  color?: ColorInput
110
175
  /** Sprite opacity 0..1 (sprite nodes only). */
111
176
  opacity?: number
@@ -126,8 +191,6 @@ export type Scene2dCameraDef = {
126
191
 
127
192
  export type Scene2dDef = {
128
193
  env?: Scene2dEnv
129
- /** Per-layer config, keyed by layer number: `layers: { 1: { ySort: true } }`. */
130
- layers?: Record<number | string, { ySort?: boolean }>
131
194
  /** Starting camera when there's no camera NODE (a camera node wins). */
132
195
  camera?: Scene2dCameraDef
133
196
  nodes?: Record<string, Scene2dNodeDef>
@@ -139,6 +202,9 @@ type SourceNodeOf<N extends Scene2dNodeDef> =
139
202
  N extends { sprite: { clip: string } } ? Sprite & { anim: SpriteAnimation }
140
203
  : N extends { sprite: SpriteSourceDef } ? Sprite
141
204
  : N extends { tilemap: TilemapSourceDef } ? Tilemap
205
+ : N extends { path: PathSourceDef } ? Path2D
206
+ : N extends { text: TextSourceDef } ? Text2D
207
+ : N extends { particles: Particles2DSourceDef } ? Particles2D
142
208
  : Node2D
143
209
 
144
210
  type AspectsOf<N extends Scene2dNodeDef> =
@@ -209,11 +275,31 @@ const createTilemap = async (
209
275
  })
210
276
  }
211
277
 
278
+ const createText = async (name: string, def: TextSourceDef): Promise<Text2D> => {
279
+ if (typeof def.font !== "string") {
280
+ throw new Error(`Scene2d node "${name}": text.font must be a string — a font("…") family or a font file URL`)
281
+ }
282
+ const { font, ...rest } = def
283
+ return new Text2D({ ...rest, font: await Font2D.load(font) })
284
+ }
285
+
286
+ const createParticles = async (name: string, def: Particles2DSourceDef): Promise<Particles2D> => {
287
+ const { texture, ...rest } = def
288
+ if (texture === undefined) return new Particles2D(rest)
289
+ if (typeof texture !== "string") {
290
+ throw new Error(`Scene2d node "${name}": particles.texture must be a string — an image URL (asset('./spark.png'))`)
291
+ }
292
+ return new Particles2D({ ...rest, texture: await Texture2D.load(texture) })
293
+ }
294
+
212
295
  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)`)
296
+ const sources = [ def.sprite, def.tilemap, def.path, def.text, def.particles, def.camera, def.make ].filter((s) => s !== undefined).length
297
+ if (sources > 1) throw new Error(`Scene2d node "${name}" declares more than one source (sprite/tilemap/path/text/particles/camera/make)`)
215
298
  if (def.sprite !== undefined) return createSprite(name, def.sprite)
216
299
  if (def.tilemap !== undefined) return createTilemap(name, def.tilemap, def.aspects)
300
+ if (def.path !== undefined) return new Path2D(def.path)
301
+ if (def.text !== undefined) return createText(name, def.text)
302
+ if (def.particles !== undefined) return createParticles(name, def.particles)
217
303
  // make/camera/group nodes are plain wrappers — make subtrees mount under them in phase 2
218
304
  return new Node2D()
219
305
  }
@@ -223,8 +309,11 @@ const applyNode = (node: Node2D, name: string, def: Scene2dNodeDef): void => {
223
309
  if (def.position) node.position = def.position
224
310
  if (def.rotation !== undefined) node.rotation = def.rotation
225
311
  if (def.scale !== undefined) node.scale = def.scale
226
- if (def.layer !== undefined) node.layer = def.layer
312
+ if (def.renderPriority !== undefined) node.renderPriority = def.renderPriority
227
313
  if (def.z !== undefined) node.z = def.z
314
+ if (def.material !== undefined && (node instanceof Sprite || node instanceof Tilemap || node instanceof Text2D || node instanceof Particles2D)) {
315
+ node.material = Material2D.sprite(def.material)
316
+ }
228
317
  if (def.visible !== undefined) node.visible = def.visible
229
318
  if (def.locked !== undefined) (node as { _sceneLocked?: boolean })._sceneLocked = def.locked
230
319
  if (def.camera !== undefined) (node as { _sceneCamera?: boolean })._sceneCamera = true
@@ -235,6 +324,7 @@ const applyNode = (node: Node2D, name: string, def: Scene2dNodeDef): void => {
235
324
  if (def.flipX !== undefined) node.flipX = def.flipX
236
325
  if (def.flipY !== undefined) node.flipY = def.flipY
237
326
  }
327
+ if (node instanceof Text2D && def.color !== undefined) node.color = def.color
238
328
  }
239
329
 
240
330
  /** The first camera-source def in file order (depth-first), or null. */
@@ -323,10 +413,7 @@ const instantiate = async (def: Scene2dDef): Promise<{ scene: Scene2D, nodes: Re
323
413
  // create bodies right after the node build). Edit mode holds physics as data — skip.
324
414
  if (env.gravity !== undefined && !isEditMode()) Physics2D.configure({ gravity: env.gravity })
325
415
 
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
- }
416
+ const scene = new Scene2D({ background: env.background, filter: env.filter, ySort: env.ySort })
330
417
 
331
418
  const nodes: Record<string, Node2D> = {}
332
419
  await buildNodes(def.nodes ?? {}, null, scene, nodes, env)
@@ -370,7 +457,7 @@ export class Scene2dHandle<D extends Scene2dDef = Scene2dDef> {
370
457
  /**
371
458
  * Define a 2D scene as data — the default export of a `.scene2d.ts` file. Returns a typed handle:
372
459
  * `const { scene, nodes } = await handle.open()` gives `nodes.<name>` typed by its source block
373
- * (Sprite / Tilemap / Node2D) with its `use(...)`d aspects attached.
460
+ * (Sprite / Tilemap / Path2D / Text2D / Particles2D / Node2D) with its `use(...)`d aspects attached.
374
461
  */
375
462
  export const defineScene2d = <const D extends Scene2dDef>(def: D): Scene2dHandle<D> => {
376
463
  const handle = new Scene2dHandle(def)
@@ -1,5 +1,5 @@
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()`).
1
+ // Collision groups — which bodies are allowed to touch which. Called groups, not layers, so the name
2
+ // never reads as DRAW order (that is `node.renderPriority` / `node.z` / `scene.ySort` in 2D).
3
3
  //
4
4
  // // game/groups.ts
5
5
  // export const player = Physics2D.addGroup()
package/src/gl/Foliage.ts CHANGED
@@ -61,13 +61,6 @@ export type FoliageOptions = {
61
61
  /** 0..1: how far the baked per-vertex sky visibility (the crown's interior, the trunk under it) darkens the ambient.
62
62
  * Default 0.7. */
63
63
  skyAo?: number
64
- /** 0..1: a leaf card's shading normal, from the card's own plane (0) to the crown's hull normal (1) — what hides that a
65
- * crown is flat quads: each card is lit as the part of the volume it sits in. Default 0.7. */
66
- canopy?: number
67
- /** The cards' normal-map strength (a strong map brings the planes back). Default 0.5. */
68
- normalMap?: number
69
- /** A card seen edge-on dissolves over this much of |N·V| — no paper edges on the silhouette. Default 0.15, 0 = none. */
70
- edgeFade?: number
71
64
  /** Coverage held over distance: the cards' alpha × (1 + coverage × mip level), so a far crown does not thin out as its
72
65
  * atlas mips away. Default 0.25, 0 = none; past ~0.5 a crown turns into a solid mass. */
73
66
  coverage?: number
@@ -89,9 +82,6 @@ const state: Required<Omit<FoliageOptions, "wind">> & { wind: Required<FoliageWi
89
82
  translucency: 0.35,
90
83
  leafShadow: 0.6,
91
84
  skyAo: 0.7,
92
- canopy: 0.7,
93
- normalMap: 0.5,
94
- edgeFade: 0.15,
95
85
  coverage: 0.25,
96
86
  castDistance: 0,
97
87
  }
@@ -118,12 +108,7 @@ export const Foliage = {
118
108
  if (options.leafShadow !== undefined) { state.leafShadow = options.leafShadow; leaf = true }
119
109
  if (options.skyAo !== undefined) { state.skyAo = options.skyAo; leaf = true }
120
110
  if (leaf) _creator.foliageSetLeaf?.(state.shiver, state.translucency, state.leafShadow, state.skyAo)
121
- let look = false
122
- if (options.canopy !== undefined) { state.canopy = options.canopy; look = true }
123
- if (options.normalMap !== undefined) { state.normalMap = options.normalMap; look = true }
124
- if (options.edgeFade !== undefined) { state.edgeFade = options.edgeFade; look = true }
125
- if (options.coverage !== undefined) { state.coverage = options.coverage; look = true }
126
- if (look) _creator.foliageSetLook?.(state.canopy, state.normalMap, state.edgeFade, state.coverage)
111
+ if (options.coverage !== undefined) { state.coverage = options.coverage; _creator.foliageSetLook?.(state.coverage) }
127
112
  if (options.castDistance !== undefined) { state.castDistance = options.castDistance; _creator.foliageSetCast?.(state.castDistance) }
128
113
  },
129
114