lecodes-sdk 1.0.0 → 1.1.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 +18 -4
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +24 -0
- package/dist/types/gl/Light.d.ts +25 -7
- package/dist/types/gl/Lightmap.d.ts +90 -60
- package/dist/types/gl/Material.d.ts +28 -20
- package/dist/types/gl/Model.d.ts +7 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +40 -1
- package/dist/types/gl/Scene.d.ts +20 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
- package/dist/types/gl/animation/IK.d.ts +86 -30
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
- package/dist/types/gl/terrain/Terrain.d.ts +4 -2
- package/dist/types/inject.d.ts +8 -2
- package/dist/types/scene/defineScene.d.ts +44 -32
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/bridges.d.ts +226 -65
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/index.ts +2 -0
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +393 -348
- package/src/gl/Light.ts +46 -16
- package/src/gl/Lightmap.ts +439 -275
- package/src/gl/Material.ts +59 -47
- package/src/gl/Model.ts +167 -156
- package/src/gl/Node.ts +39 -0
- package/src/gl/Particles.ts +61 -2
- package/src/gl/Scene.ts +34 -1
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +482 -459
- package/src/gl/animation/IK.ts +173 -152
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/physics/Ragdoll.ts +451 -272
- package/src/gl/terrain/Terrain.ts +4 -2
- package/src/inject.ts +12 -2
- package/src/scene/defineScene.ts +72 -62
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/src/animate/animate.ts +0 -238
|
@@ -74,17 +74,47 @@ export type CompileOptions = {
|
|
|
74
74
|
* unless `header: false`; `map` is already shifted past that header. */
|
|
75
75
|
export type CompileResult = { code: string, map: string | null, entry: string }
|
|
76
76
|
|
|
77
|
+
/** The basename a local resource would be looked up by. */
|
|
78
|
+
export const localAssetBase = (path: string): string => path.split(/[\\/]/).pop() ?? path
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Runtime lookup names for `localAssets` resources — what the bundle passes to `fetchLocal` and what
|
|
82
|
+
* a host caller must copy the file to.
|
|
83
|
+
*
|
|
84
|
+
* The basename, unless two project files SHARE one: then every sharer takes its whole root-relative
|
|
85
|
+
* path with the separators folded into `__`. A flat name space is fine until it isn't, and when it
|
|
86
|
+
* isn't the failure is silent and absurd — the files are staged into one directory, so the last one
|
|
87
|
+
* copied answers for every `asset()` that ends in that name. Which one wins depends on the scan
|
|
88
|
+
* order, i.e. on nothing. (2026-09-17: a viewer project carried two asset packs that each ship
|
|
89
|
+
* `SM_Sign_01..03.glb`; the school's floor-standing "CLEANING IN PROGRESS" A-frame was drawn inside
|
|
90
|
+
* the shooting range's wall, where a flat 4 mm plate belonged.)
|
|
91
|
+
*/
|
|
92
|
+
export const localAssetNames = (entries: readonly { path: string, type: string }[]): Map<string, string> => {
|
|
93
|
+
const seen = new Map<string, number>()
|
|
94
|
+
for (const e of entries) if (e.type === "resource") {
|
|
95
|
+
const b = localAssetBase(e.path)
|
|
96
|
+
seen.set(b, (seen.get(b) ?? 0) + 1)
|
|
97
|
+
}
|
|
98
|
+
const names = new Map<string, string>()
|
|
99
|
+
for (const e of entries) if (e.type === "resource") {
|
|
100
|
+
const b = localAssetBase(e.path)
|
|
101
|
+
names.set(e.path, (seen.get(b) ?? 0) > 1 ? e.path.replace(/^[./]+/, "").replace(/[\\/]/g, "__") : b)
|
|
102
|
+
}
|
|
103
|
+
return names
|
|
104
|
+
}
|
|
105
|
+
|
|
77
106
|
/** Turn compile entries into the bundler's module map — the per-kind rules of the deployable
|
|
78
107
|
* bundle (resource → URL export, shader → per-backend URL template, .svg → SvgSource, .json →
|
|
79
108
|
* data, code verbatim). Exported for callers that build a `libraries` map from entries. */
|
|
80
109
|
export const entriesToFiles = (entries: CompileEntry[], opts: { publicUrl: string, localAssets?: boolean }): Record<string, string> => {
|
|
81
110
|
const files: Record<string, string> = {}
|
|
111
|
+
const localNames = localAssetNames(entries)
|
|
82
112
|
for (const e of entries) {
|
|
83
113
|
if (e.type === "resource") {
|
|
84
114
|
if (opts.localAssets && !e.remote) {
|
|
85
115
|
// The host resolves the asset by name at runtime (reads the local file → systemId); the
|
|
86
116
|
// "id:" prefix is the host's local-resource URL scheme (parsed by the UI + 2D texture paths).
|
|
87
|
-
const name = e.path
|
|
117
|
+
const name = localNames.get(e.path) ?? localAssetBase(e.path)
|
|
88
118
|
files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(name)}))`
|
|
89
119
|
} else {
|
|
90
120
|
files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`
|
|
@@ -37,9 +37,14 @@ export type DetectedEntry = {
|
|
|
37
37
|
export const ENTRY_CONVENTION = "/main.ts"
|
|
38
38
|
|
|
39
39
|
const isCode = (path: string) => /\.[tj]s$/.test(path) && !path.endsWith(".d.ts")
|
|
40
|
-
/** Never entries: editor plugins
|
|
41
|
-
* imports is still not the app; server files compile into their own bundle)
|
|
42
|
-
|
|
40
|
+
/** Never entries: editor plugins, server modules (docs/backend-plan.md — a `*.server.ts` nobody
|
|
41
|
+
* imports is still not the app; server files compile into their own bundle) and anything under
|
|
42
|
+
* `design/` — the board's screens are import-graph roots by construction (nothing imports a screen
|
|
43
|
+
* file; the board runs them), so without this a project whose app code is still to come had its
|
|
44
|
+
* largest design screen picked as the app and failed deep inside it ("asset not found:
|
|
45
|
+
* ../assets/figma/…"). A design-only project has no entry, and says so. */
|
|
46
|
+
const isNonEntry = (path: string) =>
|
|
47
|
+
path.endsWith(".editor.ts") || /\.server\.[tj]s$/.test(path) || /^\/design\//.test(path)
|
|
43
48
|
|
|
44
49
|
const normalize = (path: string): string => {
|
|
45
50
|
const parts: string[] = []
|
package/src/compile/index.ts
CHANGED
|
@@ -210,11 +210,17 @@ export type CompileServerOptions = {
|
|
|
210
210
|
export const compileServerBundle = async (opts: CompileServerOptions): Promise<{ code: string, manifest: ServerManifest } | null> => {
|
|
211
211
|
const modules = scanServerModules(opts.entries)
|
|
212
212
|
if (Object.keys(modules).length === 0) return null
|
|
213
|
+
// The server bundle sees the whole project map (a server module may import shared code), but the
|
|
214
|
+
// bundler's asset check runs over EVERY file in the map — so binaries and svgs must be present as
|
|
215
|
+
// stubs, or a client screen's `asset("./x.png")` fails the SERVER build ("asset not found", seen
|
|
216
|
+
// with a Figma-exported design next to app.server.ts). The board's `design/` files are never
|
|
217
|
+
// server code and stay out entirely.
|
|
213
218
|
const files: MemoryFiles = {}
|
|
214
219
|
for (const e of opts.entries) {
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
|
|
220
|
+
const path = normalizePath(e.path)
|
|
221
|
+
if (/^\/design\//.test(path)) continue
|
|
222
|
+
if (e.type !== "text" || path.toLowerCase().endsWith(".svg")) { files[path] = `export default ""`; continue }
|
|
223
|
+
files[path] = e.text ?? ""
|
|
218
224
|
}
|
|
219
225
|
let params = opts.params
|
|
220
226
|
if (!params && opts.validate !== false) {
|
package/src/g2/Node2D.ts
CHANGED
|
@@ -16,6 +16,19 @@ import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
|
|
|
16
16
|
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
17
17
|
import { ensurePointerEvents } from "./touch"
|
|
18
18
|
import { ensurePhysicsEvents } from "./loop"
|
|
19
|
+
import type { Animation } from "../animate/tween/Animation"
|
|
20
|
+
import { CLOCK_GAME, DOM_NODE2D, floatValue, vec2Value, type TweenChannel, type TweenMeta } from "../animate/tween/spec"
|
|
21
|
+
import { tweenBag } from "../animate/tween/Timeline"
|
|
22
|
+
|
|
23
|
+
/** Animatable transform props of a 2D node; an array of values is a keyframe list. */
|
|
24
|
+
export type Node2DTweenProps = {
|
|
25
|
+
x?: number | number[],
|
|
26
|
+
y?: number | number[],
|
|
27
|
+
position?: Vec2Like | Vec2Like[],
|
|
28
|
+
/** Degrees. */
|
|
29
|
+
rotation?: number | number[],
|
|
30
|
+
scale?: number | Vec2Like | (number | Vec2Like)[],
|
|
31
|
+
}
|
|
19
32
|
|
|
20
33
|
// re-exported so existing `import { Vec2 } from "./Node2D"` sites keep working; the canonical home is math/.
|
|
21
34
|
export { Vec2, type Vec2Like }
|
|
@@ -135,6 +148,31 @@ export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
|
|
|
135
148
|
_creator2d.setScale(this.id, sx, sy)
|
|
136
149
|
}
|
|
137
150
|
|
|
151
|
+
// --- keyframe animation (docs/timeline-plan.md) ---
|
|
152
|
+
/** Tween the transform — `node.animateTo({ x: 120, rotation: 90, duration: 400, easing: 'outBack' })`;
|
|
153
|
+
* arrays are keyframes. Game clock unless `clock: 'ui'`. Returns the {@link Animation} handle. */
|
|
154
|
+
animateTo(props: Node2DTweenProps & TweenMeta): Animation {
|
|
155
|
+
return tweenBag(this, props as Record<string, unknown>, true)
|
|
156
|
+
}
|
|
157
|
+
/** Tween FROM the given values to the node's current transform. */
|
|
158
|
+
animateFrom(props: Node2DTweenProps & TweenMeta): Animation {
|
|
159
|
+
return tweenBag(this, props as Record<string, unknown>, false)
|
|
160
|
+
}
|
|
161
|
+
/** @internal keyframe-core target protocol */
|
|
162
|
+
readonly _tweenClock: number = CLOCK_GAME
|
|
163
|
+
/** @internal */
|
|
164
|
+
_tweenChannel(prop: string): TweenChannel | null {
|
|
165
|
+
const id = () => this.id
|
|
166
|
+
switch (prop) {
|
|
167
|
+
case "x": return { domain: DOM_NODE2D, id, value: floatValue, commit: (v) => { this._x = v as number } }
|
|
168
|
+
case "y": return { domain: DOM_NODE2D, id, value: floatValue, commit: (v) => { this._y = v as number } }
|
|
169
|
+
case "position": return { domain: DOM_NODE2D, id, value: vec2Value, commit: (v) => { this._x = cx(v as Vec2Like); this._y = cy(v as Vec2Like) } }
|
|
170
|
+
case "rotation": return { domain: DOM_NODE2D, id, value: floatValue, commit: (v) => { this._rotation = v as number } }
|
|
171
|
+
case "scale": return { domain: DOM_NODE2D, id, value: vec2Value, commit: (v) => { const s = typeof v === "number" ? [v, v] : v as Vec2Like; this._sx = cx(s); this._sy = cy(s) } }
|
|
172
|
+
default: return null
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
138
176
|
/** Draw layer (int). Higher layers render on top. */
|
|
139
177
|
get layer(): number { return this._layer }
|
|
140
178
|
set layer(v: number) { this._layer = v; _creator2d.setLayer(this.id, v) }
|
package/src/g2/Sprite.ts
CHANGED
|
@@ -5,8 +5,16 @@
|
|
|
5
5
|
import { Color, type ColorInput } from "../core/color"
|
|
6
6
|
import { Vec2, type Vec2Like } from "../math/vec"
|
|
7
7
|
import { Canvas, isCanvas } from "../canvas/Canvas"
|
|
8
|
-
import { Node2D } from "./Node2D"
|
|
8
|
+
import { Node2D, type Node2DTweenProps } from "./Node2D"
|
|
9
9
|
import type { Texture2D } from "./Texture2D"
|
|
10
|
+
import { DOM_SPRITE2D, colorValue, floatValue, type TweenChannel, type TweenMeta } from "../animate/tween/spec"
|
|
11
|
+
import type { Animation } from "../animate/tween/Animation"
|
|
12
|
+
|
|
13
|
+
/** Animatable sprite props on top of the 2D transform. */
|
|
14
|
+
export type SpriteTweenProps = Node2DTweenProps & {
|
|
15
|
+
opacity?: number | number[],
|
|
16
|
+
color?: ColorInput | ColorInput[],
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
/** Anything that can back a sprite: a texture, or a Canvas (baked to a texture on assign). */
|
|
12
20
|
export type TextureSource = Texture2D | Canvas
|
|
@@ -76,6 +84,17 @@ export class Sprite extends Node2D {
|
|
|
76
84
|
return this
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
/** Tween `opacity` / `color` and the transform — see {@link Node2D.animateTo}. */
|
|
88
|
+
override animateTo(props: SpriteTweenProps & TweenMeta): Animation { return super.animateTo(props) }
|
|
89
|
+
override animateFrom(props: SpriteTweenProps & TweenMeta): Animation { return super.animateFrom(props) }
|
|
90
|
+
/** @internal */
|
|
91
|
+
override _tweenChannel(prop: string): TweenChannel | null {
|
|
92
|
+
const id = () => this.id
|
|
93
|
+
if (prop === "opacity") return { domain: DOM_SPRITE2D, id, value: floatValue, commit: (v) => { this._opacity = v as number } }
|
|
94
|
+
if (prop === "color") return { domain: DOM_SPRITE2D, id, value: (v) => colorValue(v, Color.toRgba01), commit: (v) => { this._color = v as ColorInput } }
|
|
95
|
+
return super._tweenChannel(prop)
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
get color(): ColorInput { return this._color }
|
|
80
99
|
set color(c: ColorInput) { this._color = c; const [ r, g, b ] = Color.toRgb01(c); _creator2d.setSpriteColor(this.id, r, g, b) }
|
|
81
100
|
|
package/src/gl/Camera.ts
CHANGED
|
@@ -3,8 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
import { Vec3 } from "../math/vec"
|
|
5
5
|
import { Aspect } from "../core/Aspect"
|
|
6
|
-
import { Node } from "./Node"
|
|
6
|
+
import { Node, type NodeTweenProps } from "./Node"
|
|
7
7
|
import { Ray } from "./Ray"
|
|
8
|
+
import { DOM_GL_CAMERA, floatValue, type TweenChannel, type TweenMeta } from "../animate/tween/spec"
|
|
9
|
+
import type { Animation } from "../animate/tween/Animation"
|
|
10
|
+
|
|
11
|
+
/** Animatable camera props on top of the node transform. */
|
|
12
|
+
export type CameraTweenProps = NodeTweenProps & {
|
|
13
|
+
fov?: number | number[],
|
|
14
|
+
near?: number | number[],
|
|
15
|
+
far?: number | number[],
|
|
16
|
+
}
|
|
8
17
|
|
|
9
18
|
/** @internal `camera.follow(node)`: copies the node's world pose onto the camera every LATE frame
|
|
10
19
|
* (order 1000 — after every other aspect has moved things). One per camera; lives on the target. */
|
|
@@ -24,6 +33,24 @@ export const CAMERA_DEFAULTS = { fov: 60, near: 0.01, far: 1000 }
|
|
|
24
33
|
|
|
25
34
|
export class Camera extends Node {
|
|
26
35
|
private _sceneId = -1
|
|
36
|
+
|
|
37
|
+
/** Tween `fov` / `near` / `far` (a zoom, a dolly) and the transform — see {@link Node.animateTo}. */
|
|
38
|
+
override animateTo(props: CameraTweenProps & TweenMeta): Animation { return super.animateTo(props) }
|
|
39
|
+
override animateFrom(props: CameraTweenProps & TweenMeta): Animation { return super.animateFrom(props) }
|
|
40
|
+
/** @internal The projection channels address the SCENE (one camera per scene on the host). */
|
|
41
|
+
override _tweenChannel(prop: string): TweenChannel | null {
|
|
42
|
+
if (prop === "fov" || prop === "near" || prop === "far") {
|
|
43
|
+
const id = () => (this._sceneId < 0 ? 0 : this._sceneId + 1) // scene ids start at 0; 0 = unaddressable
|
|
44
|
+
const commit = (v: unknown) => {
|
|
45
|
+
if (prop === "fov") this._fov = v as number
|
|
46
|
+
else if (prop === "near") this._near = v as number
|
|
47
|
+
else this._far = v as number
|
|
48
|
+
this._projSet = true
|
|
49
|
+
}
|
|
50
|
+
return { domain: DOM_GL_CAMERA, id, value: floatValue, commit }
|
|
51
|
+
}
|
|
52
|
+
return super._tweenChannel(prop)
|
|
53
|
+
}
|
|
27
54
|
/** Live projection matrix (updated by the host when the viewport/fov changes). */
|
|
28
55
|
readonly projectionMatrix = new Float32Array(16)
|
|
29
56
|
|
|
@@ -57,6 +84,12 @@ export class Camera extends Node {
|
|
|
57
84
|
* adaptation when the sun swings into view (drop it, and the sun stays at the display's peak
|
|
58
85
|
* while everything else darkens). Scene light follows it; particle `emissive` is post-exposure
|
|
59
86
|
* and does not, so a fireball keeps its on-screen brightness. Cheap to animate every frame. */
|
|
87
|
+
/** The camera's EV100 with the compensation in: log2(aperture² / shutterSpeed x 100 / iso) - compensation. Sunny 16 is
|
|
88
|
+
* ~15; a lit surface of luminance L lands on screen at L / (1.2 x 2^ev100). */
|
|
89
|
+
get ev100(): number {
|
|
90
|
+
const e = this._exposure
|
|
91
|
+
return Math.log2((e.aperture * e.aperture) / e.shutterSpeed * 100 / e.iso) - this._ev
|
|
92
|
+
}
|
|
60
93
|
get exposureCompensation(): number { return this._ev }
|
|
61
94
|
set exposureCompensation(stops: number) {
|
|
62
95
|
this._ev = Number.isFinite(stops) ? stops : 0
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Foliage — the vegetation tier of the lightmap material family (creator-gl materials/src/foliage.mat).
|
|
2
|
+
//
|
|
3
|
+
// A model loaded with `Model.load(src, { foliage: true })` (scene files: `foliage: true` on a `model`
|
|
4
|
+
// node) takes a shader whose vertices sway in the wind, bend away from BENDERS (the hero, enemies —
|
|
5
|
+
// any node registered with `Foliage.bend`) and thin out over a distance range, and whose fragment
|
|
6
|
+
// tints every copy a little differently. All of it is level-wide state the engine keeps: the wind
|
|
7
|
+
// and the sway options apply to every foliage model, present and future; benders are read from
|
|
8
|
+
// their live world transforms each frame (the 8 nearest the camera reach the shader); a fade is per
|
|
9
|
+
// ASSET — every copy of that model, present and future.
|
|
10
|
+
//
|
|
11
|
+
// Hosts without the tier (no `foliageSet*` bridge) ignore every call; the models then load through
|
|
12
|
+
// the ordinary lightmap tier where that exists, else the standard shader.
|
|
13
|
+
import { Model } from "./Model"
|
|
14
|
+
import { Node } from "./Node"
|
|
15
|
+
|
|
16
|
+
export type FoliageWind = {
|
|
17
|
+
/** Direction on the ground (x, z); normalised by the engine. Default [1, 0.3]. */
|
|
18
|
+
direction?: [number, number]
|
|
19
|
+
/** Metres of sway at `bendHeight` (taller parts sway more). Default 0.07. 0 = still. */
|
|
20
|
+
strength?: number
|
|
21
|
+
/** Radians per second of the main sway. Default 1.2. */
|
|
22
|
+
speed?: number
|
|
23
|
+
/** 0..1: how much the strength breathes over a slow envelope. Default 0.6. */
|
|
24
|
+
gust?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type FoliageOptions = {
|
|
28
|
+
/** The wind, or `false` for none. */
|
|
29
|
+
wind?: FoliageWind | false
|
|
30
|
+
/** Height above the instance origin (m) where the full sway / touch applies; the base never moves.
|
|
31
|
+
* Default 1.5. */
|
|
32
|
+
bendHeight?: number
|
|
33
|
+
/** Metres the TOP of a small plant leans away from a bender standing at its centre; the whole plant leans
|
|
34
|
+
* as one, rooted at its base. Plants over ~2 m react less, trees over ~5 m not at all (the engine
|
|
35
|
+
* measures each asset). Default 0.6. 0 = no touch reaction. */
|
|
36
|
+
touch?: number
|
|
37
|
+
/** 0..1: how far each copy's tint drifts from the texture (warmer/darker to cooler/lighter, by a hash
|
|
38
|
+
* of its position). Default 0.35. */
|
|
39
|
+
variation?: number
|
|
40
|
+
/** 0..1: wrapped sun lighting — thin leaves let light through. Default 0.5. */
|
|
41
|
+
sunWrap?: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const state: Required<Omit<FoliageOptions, "wind">> & { wind: Required<FoliageWind> | false } = {
|
|
45
|
+
wind: { direction: [ 1, 0.3 ], strength: 0.07, speed: 1.2, gust: 0.6 },
|
|
46
|
+
bendHeight: 1.5,
|
|
47
|
+
touch: 0.6,
|
|
48
|
+
variation: 0.35,
|
|
49
|
+
sunWrap: 0.5,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pushWind = (): void => {
|
|
53
|
+
const w = state.wind
|
|
54
|
+
if (w === false) _creator.foliageSetWind?.(1, 0, 0, 1, 0)
|
|
55
|
+
else _creator.foliageSetWind?.(w.direction[0], w.direction[1], w.strength, w.speed, w.gust)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const Foliage = {
|
|
59
|
+
/** Level-wide options; each field given replaces the current value (a scene file's `env.foliage`
|
|
60
|
+
* goes through here). */
|
|
61
|
+
configure(options: FoliageOptions): void {
|
|
62
|
+
if (options.wind !== undefined) Foliage.wind(options.wind)
|
|
63
|
+
let sway = false
|
|
64
|
+
if (options.bendHeight !== undefined) { state.bendHeight = options.bendHeight; sway = true }
|
|
65
|
+
if (options.touch !== undefined) { state.touch = options.touch; sway = true }
|
|
66
|
+
if (options.variation !== undefined) { state.variation = options.variation; sway = true }
|
|
67
|
+
if (options.sunWrap !== undefined) { state.sunWrap = options.sunWrap; sway = true }
|
|
68
|
+
if (sway) _creator.foliageSetOptions?.(state.bendHeight, state.touch, state.variation, state.sunWrap)
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/** The wind: fields given replace the current ones; `false` = calm. Live. */
|
|
72
|
+
wind(wind: FoliageWind | false): void {
|
|
73
|
+
if (wind === false) state.wind = false
|
|
74
|
+
else {
|
|
75
|
+
const cur = state.wind === false ? { direction: [ 1, 0.3 ] as [number, number], strength: 0.07, speed: 1.2, gust: 0.6 } : state.wind
|
|
76
|
+
state.wind = { direction: wind.direction ?? cur.direction, strength: wind.strength ?? cur.strength, speed: wind.speed ?? cur.speed, gust: wind.gust ?? cur.gust }
|
|
77
|
+
}
|
|
78
|
+
pushWind()
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/** A node whose live position pushes the vegetation within `radius` metres (the hero: ~0.7; a
|
|
82
|
+
* boulder: its size). Any number may be registered; the 8 nearest the camera act each frame. The
|
|
83
|
+
* engine also tracks the node's velocity (smoothed over ~0.2 s): a plant you run through is brushed
|
|
84
|
+
* forward and settles behind you instead of flipping as you cross its centre.
|
|
85
|
+
* Returns the unregister function; a destroyed node drops out on its own. */
|
|
86
|
+
bend(node: Node, radius = 0.7): () => void {
|
|
87
|
+
_creator.foliageSetBender?.(node.id, radius > 0 ? radius : 0.01)
|
|
88
|
+
return () => Foliage.unbend(node)
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/** Stop a node from bending the vegetation. */
|
|
92
|
+
unbend(node: Node): void {
|
|
93
|
+
_creator.foliageSetBender?.(node.id, 0)
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/** Distance fade for EVERY copy of this model's asset (present and future): the cards thin out
|
|
97
|
+
* from `start` to `end` metres from the camera and past `end` the copy leaves the frame entirely.
|
|
98
|
+
* Small ground cover (grass, clover, flowers) is the target; `end` 0 = no fade. */
|
|
99
|
+
fade(model: Model, start: number, end: number): void {
|
|
100
|
+
_creator.foliageSetFade?.(model.id, start, end)
|
|
101
|
+
},
|
|
102
|
+
}
|