lecodes-sdk 2.4.10 → 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.
- package/dist/global.d.ts +35 -0
- package/dist/types/core/env.d.ts +25 -0
- package/dist/types/core/particlesConfig.d.ts +196 -0
- package/dist/types/g2/Font2D.d.ts +19 -0
- package/dist/types/g2/Material2D.d.ts +24 -0
- package/dist/types/g2/Node2D.d.ts +6 -5
- package/dist/types/g2/Particles2D.d.ts +169 -0
- package/dist/types/g2/Path2D.d.ts +95 -0
- package/dist/types/g2/Physics2D.d.ts +1 -1
- package/dist/types/g2/Scene2D.d.ts +15 -17
- package/dist/types/g2/Sprite.d.ts +12 -1
- package/dist/types/g2/SpriteSheet.d.ts +4 -1
- package/dist/types/g2/Text2D.d.ts +96 -0
- package/dist/types/g2/Tilemap.d.ts +8 -0
- package/dist/types/g2/defineScene2d.d.ts +69 -11
- package/dist/types/gl/Foliage.d.ts +0 -7
- package/dist/types/gl/Lightmap.d.ts +101 -2
- package/dist/types/gl/Material.d.ts +21 -15
- package/dist/types/gl/Model.d.ts +8 -3
- package/dist/types/gl/Particles.d.ts +3 -135
- package/dist/types/gl/Statics.d.ts +56 -0
- package/dist/types/gl/staticsFormat.d.ts +101 -0
- package/dist/types/inject.d.ts +11 -3
- package/dist/types/runtime/env.d.ts +10 -0
- package/dist/types/runtime/envValues.d.ts +8 -0
- package/dist/types/scene/defineScene.d.ts +17 -12
- package/dist/types/server/env.d.ts +14 -0
- package/dist/types/server/inject.d.ts +1 -0
- package/dist/types/ui/fonts.d.ts +4 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/bridges/2d.d.ts +65 -5
- package/src/bridges/gl.d.ts +34 -15
- package/src/compile/bakeMacro.ts +3 -0
- package/src/compile/compileProject.ts +25 -4
- package/src/compile/envCompile.ts +74 -0
- package/src/compile/fontMacro.ts +11 -6
- package/src/compile/index.ts +6 -0
- package/src/compile/resourceSlots.ts +1 -0
- package/src/compile/serverSplit.ts +47 -4
- package/src/core/env.ts +47 -0
- package/src/core/particlesConfig.ts +422 -0
- package/src/g2/Font2D.ts +87 -0
- package/src/g2/Material2D.ts +64 -0
- package/src/g2/Node2D.ts +10 -6
- package/src/g2/Particles2D.ts +337 -0
- package/src/g2/Path2D.ts +215 -0
- package/src/g2/Physics2D.ts +1 -1
- package/src/g2/Scene2D.ts +16 -22
- package/src/g2/Sprite.ts +23 -4
- package/src/g2/SpriteSheet.ts +9 -5
- package/src/g2/Text2D.ts +178 -0
- package/src/g2/Tilemap.ts +10 -0
- package/src/g2/defineScene2d.ts +108 -21
- package/src/g2/groups2d.ts +2 -2
- package/src/gl/Foliage.ts +1 -16
- package/src/gl/Lightmap.ts +70 -26
- package/src/gl/Material.ts +51 -21
- package/src/gl/Model.ts +19 -5
- package/src/gl/Particles.ts +22 -361
- package/src/gl/Statics.ts +301 -0
- package/src/gl/nav/NavMesh.ts +4 -1
- package/src/gl/staticsFormat.ts +240 -0
- package/src/inject.ts +18 -3
- package/src/runtime/env.ts +13 -0
- package/src/runtime/envValues.ts +8 -0
- package/src/runtime/fetch.ts +18 -0
- package/src/scene/defineScene.ts +107 -27
- package/src/server/env.ts +25 -0
- package/src/server/host.ts +6 -0
- package/src/server/inject.ts +2 -1
- package/src/ui/fonts.ts +46 -0
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
63
|
-
_creator2d.setSprite(this.id, tex.id
|
|
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 }
|
package/src/g2/SpriteSheet.ts
CHANGED
|
@@ -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]
|
|
9
|
-
// scene.add(props.make('water', { position: [0, 0] }))
|
|
10
|
-
// const player = hero.make('hero', { clip: 'idle'
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/src/g2/Text2D.ts
ADDED
|
@@ -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
|
package/src/g2/defineScene2d.ts
CHANGED
|
@@ -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), `
|
|
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('…') },
|
|
16
|
-
// tree1: { sprite: { sheet: props, name: 'tree_orange' }, position: [160, 288]
|
|
17
|
-
//
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
105
|
-
|
|
106
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
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)
|
package/src/g2/groups2d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Collision groups — which bodies are allowed to touch which. Called groups, not layers,
|
|
2
|
-
//
|
|
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
|
-
|
|
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
|
|